Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,17 @@ Extend the channel consumer of django-loci in this way:
class LocationBroadcast(BaseLocationBroadcast):
model = Location

Extend the broadcast consumer for all locations:

.. code-block:: python

from django_loci.channels.base import BaseCommonLocationBroadcast
from ..models import Location # your own location model


class CommonLocationBroadcast(BaseCommonLocationBroadcast):
model = Location

Extending AppConfig
~~~~~~~~~~~~~~~~~~~

Expand Down
14 changes: 11 additions & 3 deletions django_loci/channels/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from django.core.asgi import get_asgi_application
from django.urls import path

from django_loci.channels.base import location_broadcast_path
from django_loci.channels.consumers import LocationBroadcast
from django_loci.channels.base import (
common_location_broadcast_path,
location_broadcast_path,
)
from django_loci.channels.consumers import CommonLocationBroadcast, LocationBroadcast

channel_routing = ProtocolTypeRouter(
{
Expand All @@ -17,7 +20,12 @@
location_broadcast_path,
LocationBroadcast.as_asgi(),
name="LocationChannel",
)
),
path(
common_location_broadcast_path,
CommonLocationBroadcast.as_asgi(),
name="AllLocationChannel",
),
]
)
)
Expand Down
24 changes: 23 additions & 1 deletion django_loci/channels/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from channels.generic.websocket import JsonWebsocketConsumer
from django.core.exceptions import ValidationError

location_broadcast_path = "ws/loci/location/<uuid:pk>/"
location_broadcast_path = "ws/loci/locations/<uuid:pk>/"
common_location_broadcast_path = "ws/loci/locations/"


def _get_object_or_none(model, **kwargs):
Expand Down Expand Up @@ -61,3 +62,24 @@ def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.group_name, self.channel_name
)


class BaseCommonLocationBroadcast(BaseLocationBroadcast):

def connect(self):
"""
Modified connect to handle all locations subscription without location pk
"""
try:
user = self.scope["user"]
except KeyError:
self.close()
else:
if not self.is_authorized(user, None):
self.close()
return
self.accept()
self.group_name = "loci.mobile-location.common"
async_to_sync(self.channel_layer.group_add)(
self.group_name, self.channel_name
)
6 changes: 5 additions & 1 deletion django_loci/channels/consumers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from ..models import Location
from .base import BaseLocationBroadcast
from .base import BaseCommonLocationBroadcast, BaseLocationBroadcast


class LocationBroadcast(BaseLocationBroadcast):
model = Location


class CommonLocationBroadcast(BaseCommonLocationBroadcast):
model = Location
37 changes: 31 additions & 6 deletions django_loci/channels/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,40 @@


def update_mobile_location(sender, instance, **kwargs):
"""
Sends WebSocket updates when a location record is updated.
- Sends a message to the location specific group.
- Sends a message to a common group for tracking all mobile location updates.
"""
if not kwargs.get("created") and instance.geometry:
group_name = "loci.mobile-location.{0}".format(str(instance.pk))
channel_layer = channels.layers.get_channel_layer()
message = {
"geometry": json.loads(instance.geometry.geojson),
"address": instance.address,
}

# Send update to location specific group
async_to_sync(channel_layer.group_send)(
f"loci.mobile-location.{instance.pk}",
{
"type": "send_message",
"message": {
"geometry": json.loads(instance.geometry.geojson),
"address": instance.address,
},
},
)

# Send update to common mobile location group
async_to_sync(channel_layer.group_send)(
group_name, {"type": "send_message", "message": message}
"loci.mobile-location.common",
{
"type": "send_message",
"message": {
"id": str(instance.pk),
"geometry": json.loads(instance.geometry.geojson),
"address": instance.address,
"name": instance.name,
"type": instance.type,
"is_mobile": instance.is_mobile,
},
},
)


Expand Down
2 changes: 1 addition & 1 deletion django_loci/static/django-loci/js/loci.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ django.jQuery(function ($) {
var host = window.location.host,
protocol = window.location.protocol === "http:" ? "ws" : "wss",
ws = new ReconnectingWebSocket(
protocol + "://" + host + "/ws/loci/location/" + pk + "/",
protocol + "://" + host + "/ws/loci/locations/" + pk + "/",
);
ws.onmessage = function (e) {
const data = JSON.parse(e.data);
Expand Down
154 changes: 128 additions & 26 deletions django_loci/tests/base/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.contrib.auth.models import Permission
from django.http.request import HttpRequest

from django_loci.channels.consumers import LocationBroadcast
from django_loci.channels.consumers import CommonLocationBroadcast, LocationBroadcast

from ...channels.base import _get_object_or_none
from .. import TestAdminMixin, TestLociMixin
Expand All @@ -30,7 +30,7 @@ async def _force_login(self, user, backend=None):
request.session.save
return request.session

async def _get_request_dict(self, pk=None, user=None):
async def _get_specific_location_request_dict(self, pk=None, user=None):
if not pk:
location = await database_sync_to_async(self._create_location)(
is_mobile=True
Expand All @@ -39,26 +39,48 @@ async def _get_request_dict(self, pk=None, user=None):
location=location
)
pk = location.pk
path = "/ws/loci/location/{0}/".format(pk)
path = "/ws/loci/locations/{0}/".format(pk)
session = None
if user:
session = await self._force_login(user)
return {"pk": pk, "path": path, "session": session}

def _get_communicator(self, request_vars, user=None):
async def _get_common_location_request_dict(self, user=None):
location = await database_sync_to_async(self._create_location)(is_mobile=True)
await database_sync_to_async(self._create_object_location)(location=location)
pk = location.pk
path = "/ws/loci/locations/"
session = None
if user:
session = await self._force_login(user)
return {"pk": pk, "path": path, "session": session}

def _get_location_communicator(
self, consumer_cls, request_vars, user=None, include_pk=False
):
communicator = WebsocketCommunicator(
LocationBroadcast.as_asgi(), request_vars["path"]
consumer_cls.as_asgi(), request_vars["path"]
)
if user:
communicator.scope.update(
{
"user": user,
"session": request_vars["session"],
"url_route": {"kwargs": {"pk": request_vars["pk"]}},
}
)
scope = {
"user": user,
"session": request_vars["session"],
}
if include_pk:
scope["url_route"] = {"kwargs": {"pk": request_vars["pk"]}}
communicator.scope.update(scope)
return communicator

def _get_specific_location_communicator(self, request_vars, user=None):
return self._get_location_communicator(
LocationBroadcast, request_vars, user=user, include_pk=True
)

def _get_common_location_communicator(self, request_vars, user=None):
return self._get_location_communicator(
CommonLocationBroadcast, request_vars, user=user, include_pk=False
)

@pytest.mark.django_db(transaction=True)
def test_object_or_none(self):
result = _get_object_or_none(self.location_model, pk=1)
Expand All @@ -70,8 +92,17 @@ def test_object_or_none(self):
@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_consumer_unauthenticated(self):
request_vars = await self._get_request_dict()
communicator = self._get_communicator(request_vars)
request_vars = await self._get_specific_location_request_dict()
communicator = self._get_specific_location_communicator(request_vars)
connected, _ = await communicator.connect()
assert not connected
await communicator.disconnect()

@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_common_location_consumer_unauthenticated(self):
request_vars = await self._get_common_location_request_dict()
communicator = self._get_common_location_communicator(request_vars)
connected, _ = await communicator.connect()
assert not connected
await communicator.disconnect()
Expand All @@ -80,8 +111,18 @@ async def test_consumer_unauthenticated(self):
@pytest.mark.django_db(transaction=True)
async def test_connect_admin(self):
test_user = await database_sync_to_async(self._create_admin)()
request_vars = await self._get_request_dict(user=test_user)
communicator = self._get_communicator(request_vars, test_user)
request_vars = await self._get_specific_location_request_dict(user=test_user)
communicator = self._get_specific_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert connected
await communicator.disconnect()

@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_common_location_connect_admin(self):
test_user = await database_sync_to_async(self._create_admin)()
request_vars = await self._get_common_location_request_dict(user=test_user)
communicator = self._get_common_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert connected
await communicator.disconnect()
Expand All @@ -92,8 +133,20 @@ async def test_consumer_not_staff(self):
test_user = await database_sync_to_async(self.user_model.objects.create_user)(
username="user", password="password", email="[email protected]"
)
request_vars = await self._get_request_dict(user=test_user)
communicator = self._get_communicator(request_vars, test_user)
request_vars = await self._get_specific_location_request_dict(user=test_user)
communicator = self._get_specific_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert not connected
await communicator.disconnect()

@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_common_location_consumer_not_staff(self):
test_user = await database_sync_to_async(self.user_model.objects.create_user)(
username="user", password="password", email="[email protected]"
)
request_vars = await self._get_common_location_request_dict(user=test_user)
communicator = self._get_common_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert not connected
await communicator.disconnect()
Expand All @@ -103,8 +156,8 @@ async def test_consumer_not_staff(self):
async def test_consumer_404(self):
pk = self.location_model().pk
admin = await database_sync_to_async(self._create_admin)()
request_vars = await self._get_request_dict(pk=pk, user=admin)
communicator = self._get_communicator(request_vars, admin)
request_vars = await self._get_specific_location_request_dict(pk=pk, user=admin)
communicator = self._get_specific_location_communicator(request_vars, admin)
connected, _ = await communicator.connect()
assert not connected

Expand All @@ -119,16 +172,45 @@ async def test_consumer_staff_but_no_change_permission(self):
location=location
)
pk = ol.location.pk
request_vars = await self._get_request_dict(pk=pk, user=test_user)
communicator = self._get_communicator(request_vars, test_user)
request_vars = await self._get_specific_location_request_dict(
pk=pk, user=test_user
)
communicator = self._get_specific_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert not connected
await communicator.disconnect()

# add permission to change location and repeat
loc_perm = await Permission.objects.filter(
codename=f"change_{self.location_model._meta.model_name}"
).afirst()
await test_user.user_permissions.aadd(loc_perm)
test_user = await self.user_model.objects.aget(pk=test_user.pk)
communicator = self._get_specific_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert connected
await communicator.disconnect()

@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_common_location_consumer_staff_but_no_change_permission(self):
test_user = await database_sync_to_async(self.user_model.objects.create_user)(
username="user", password="password", email="[email protected]", is_staff=True
)
location = await database_sync_to_async(self._create_location)(is_mobile=True)
await database_sync_to_async(self._create_object_location)(location=location)
request_vars = await self._get_common_location_request_dict(user=test_user)
communicator = self._get_common_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert not connected
await communicator.disconnect()
# add permission to change location and repeat
loc_perm = await Permission.objects.filter(name="Can change location").afirst()
loc_perm = await Permission.objects.filter(
codename=f"change_{self.location_model._meta.model_name}"
).afirst()
await test_user.user_permissions.aadd(loc_perm)
test_user = await self.user_model.objects.aget(pk=test_user.pk)
communicator = self._get_communicator(request_vars, test_user)
communicator = self._get_common_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert connected
await communicator.disconnect()
Expand All @@ -137,15 +219,35 @@ async def test_consumer_staff_but_no_change_permission(self):
@pytest.mark.django_db(transaction=True)
async def test_location_update(self):
test_user = await database_sync_to_async(self._create_admin)()
request_vars = await self._get_request_dict(user=test_user)
communicator = self._get_communicator(request_vars, test_user)
request_vars = await self._get_specific_location_request_dict(user=test_user)
communicator = self._get_specific_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert connected
await self._save_location(request_vars["pk"])
response = await communicator.receive_json_from()
assert response == {
"geometry": {"type": "Point", "coordinates": [12.513124, 41.897903]},
"address": "Via del Corso, Roma, Italia",
}
await communicator.disconnect()

@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_common_location_update(self):
test_user = await database_sync_to_async(self._create_admin)()
request_vars = await self._get_common_location_request_dict(user=test_user)
communicator = self._get_common_location_communicator(request_vars, test_user)
connected, _ = await communicator.connect()
assert connected
await self._save_location(request_vars["pk"])
response = await communicator.receive_json_from()
assert response == {
"id": str(request_vars["pk"]),
"geometry": {"type": "Point", "coordinates": [12.513124, 41.897903]},
"address": "Via del Corso, Roma, Italia",
"name": "test-location",
"type": "outdoor",
"is_mobile": True,
}
await communicator.disconnect()

Expand Down
Loading
Loading