-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
11 changed files
with
213 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -94,4 +94,51 @@ This can help render `turbo-cable-stream-source` in Django template | |
|
||
`<turbo-cable-stream-source>` is a custom element provided by [turbo-rails](https://github.com/hotwired/turbo-rails/blob/097d8f90cf0c5ed24ac6b1a49cead73d49fa8ab5/app/javascript/turbo/cable_stream_source_element.js), with it, we can send Turbo Stream over the websocket connection and update the page in real time. | ||
|
||
The `<turbo-cable-stream-source>` is built on Rails ActionCable, which provide many great feature out of the box, such as `Automatic Reconnection`, so we can focus on the business logic. | ||
To import `turbo-cable-stream-source` element to the frontend, there are two ways: | ||
|
||
```html | ||
<script type="module"> | ||
import 'https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm' | ||
</script> | ||
``` | ||
|
||
Or you can [Jump start frontend project bundled by Webpack](https://github.com/AccordBox/python-webpack-boilerplate#jump-start-frontend-project-bundled-by-webpack) and install it via `npm install` | ||
|
||
After frontend work is done, to support Actioncable on the server, please install [django-actioncable](https://github.com/AccordBox/django-actioncable). | ||
|
||
In `routing.py`, register `TurboStreamCableChannel` | ||
|
||
```python | ||
from actioncable import cable_channel_register | ||
from turbo_response.cable_channel import TurboStreamCableChannel | ||
|
||
cable_channel_register(TurboStreamCableChannel) | ||
``` | ||
|
||
In Django template, we can subscribe to stream source like this: | ||
|
||
```html | ||
{% load turbo_helper %} | ||
|
||
{% turbo_stream_from 'chat' view.kwargs.chat_pk %} | ||
``` | ||
|
||
`turbo_stream_from` can accept multiple positional arguments | ||
|
||
Then in Python code, we can send Turbo Stream to the stream source like this | ||
|
||
```python | ||
from turbo_response.channel_helper import broadcast_render_to | ||
|
||
broadcast_render_to( | ||
["chat", instance.chat_id], | ||
template="message_append.turbo_stream.html", | ||
context={ | ||
"instance": instance, | ||
}, | ||
) | ||
``` | ||
|
||
The `["chat", instance.chat_id]` **should** match the positional arguments in the `turbo_stream_from` tag. | ||
|
||
The web page can be updated in real time, through Turbo Stream over Websocket. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from django.core.signing import Signer | ||
|
||
from .channel_helper import verify_signed_stream_key | ||
|
||
try: | ||
from actioncable import ActionCableConsumer, CableChannel | ||
except ImportError as err: | ||
raise Exception("Please make sure django-actioncable is installed") from err | ||
|
||
signer = Signer() | ||
|
||
|
||
class TurboStreamCableChannel(CableChannel): | ||
def __init__(self, consumer: ActionCableConsumer, identifier_key, params=None): | ||
self.params = params if params else {} | ||
self.identifier_key = identifier_key | ||
self.consumer = consumer | ||
self.group_name = None | ||
|
||
async def subscribe(self): | ||
flag, stream_name = verify_signed_stream_key(self.params["signed_stream_name"]) | ||
self.group_name = stream_name | ||
if flag: | ||
await self.consumer.subscribe_group(self.group_name, self) | ||
|
||
async def unsubscribe(self): | ||
await self.consumer.unsubscribe_group(self.group_name, self) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import pytest | ||
from actioncable import ActionCableConsumer, cable_channel_register, compact_encode_json | ||
from channels.layers import get_channel_layer | ||
from channels.testing import WebsocketCommunicator | ||
|
||
from turbo_response.cable_channel import TurboStreamCableChannel | ||
from turbo_response.channel_helper import generate_signed_stream_key | ||
|
||
# register the TurboStreamCableChannel | ||
cable_channel_register(TurboStreamCableChannel) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_subscribe(): | ||
communicator = WebsocketCommunicator( | ||
ActionCableConsumer.as_asgi(), "/cable", subprotocols=["actioncable-v1-json"] | ||
) | ||
connected, subprotocol = await communicator.connect(timeout=10) | ||
assert connected | ||
response = await communicator.receive_json_from() | ||
assert response == {"type": "welcome"} | ||
|
||
# Subscribe | ||
group_name = "test" | ||
subscribe_command = { | ||
"command": "subscribe", | ||
"identifier": compact_encode_json( | ||
{ | ||
"channel": TurboStreamCableChannel.__name__, | ||
"signed_stream_name": generate_signed_stream_key(group_name), | ||
} | ||
), | ||
} | ||
|
||
await communicator.send_to(text_data=compact_encode_json(subscribe_command)) | ||
response = await communicator.receive_json_from(timeout=10) | ||
assert response["type"] == "confirm_subscription" | ||
|
||
# Message | ||
channel_layer = get_channel_layer() | ||
await channel_layer.group_send( | ||
group_name, | ||
{ | ||
"type": "message", | ||
"group": group_name, | ||
"data": { | ||
"message": "html_snippet", | ||
}, | ||
}, | ||
) | ||
|
||
response = await communicator.receive_json_from(timeout=5) | ||
assert response["message"] == "html_snippet" | ||
|
||
# Unsubscribe | ||
group_name = "test" | ||
subscribe_command = { | ||
"command": "unsubscribe", | ||
"identifier": compact_encode_json( | ||
{ | ||
"channel": TurboStreamCableChannel.__name__, | ||
"signed_stream_name": generate_signed_stream_key(group_name), | ||
} | ||
), | ||
} | ||
|
||
await communicator.send_to(text_data=compact_encode_json(subscribe_command)) | ||
|
||
# Message | ||
channel_layer = get_channel_layer() | ||
await channel_layer.group_send( | ||
group_name, | ||
{ | ||
"type": "message", | ||
"group": group_name, | ||
"data": { | ||
"message": "html_snippet", | ||
}, | ||
}, | ||
) | ||
|
||
assert await communicator.receive_nothing() is True | ||
|
||
# Close | ||
await communicator.disconnect() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters