Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support django-channels 2 - this PR is looking for someone to take over and finish the work #85

Closed
wants to merge 10 commits into from
42 changes: 33 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------

Expand All @@ -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 <http://channels.readthedocs.io/>`_ 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 <https://channels.readthedocs.io/en/latest/>`_
together with Django 2.1+

pip install django-nyt --pre


Docs
Expand Down
2 changes: 1 addition & 1 deletion django_nyt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
_disable_notifications = False

__version__ = "1.1.2"
__version__ = "1.4.0"

default_app_config = "django_nyt.apps.DjangoNytConfig"
143 changes: 98 additions & 45 deletions django_nyt/consumers.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 11 additions & 7 deletions django_nyt/routing.py
Original file line number Diff line number Diff line change
@@ -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),
])
)
})
33 changes: 20 additions & 13 deletions django_nyt/subscribers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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})
15 changes: 10 additions & 5 deletions django_nyt/utils.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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
4 changes: 2 additions & 2 deletions django_nyt/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from django_nyt import __version__


packages = find_packages()

setup(
Expand Down
5 changes: 5 additions & 0 deletions test-project/testapp/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
# Empty for now (http->django views is added by default)
})
2 changes: 1 addition & 1 deletion test-project/testproject/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
Expand Down Expand Up @@ -154,3 +153,4 @@


NYT_ENABLE_ADMIN = True
ASGI_APPLICATION = "testapp.routing.application"