Skip to content

Commit

Permalink
add refresh tag (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-yin authored Jan 24, 2024
1 parent 9623f5d commit 158ef32
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 131 deletions.
17 changes: 9 additions & 8 deletions docs/source/real-time-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ In `routing.py`, register `TurboStreamCableChannel`

```python
from actioncable import cable_channel_register
from turbo_helper.cable_channel import TurboStreamCableChannel
from turbo_helper.channels.streams_channel import TurboStreamCableChannel

cable_channel_register(TurboStreamCableChannel)
```
Expand All @@ -40,15 +40,16 @@ In Django template, we can subscribe to stream source like this:
Then in Python code, we can send Turbo Stream to the stream source like this

```python
from turbo_helper.channel_helper import broadcast_render_to

from turbo_helper.channels.broadcasts import broadcast_render_to

broadcast_render_to(
"chat",
instance.chat_id,
template="message_append.turbo_stream.html",
context={
"instance": instance,
},
"chat",
instance.chat_id,
template="message_append.turbo_stream.html",
context={
"instance": instance,
},
)
```

Expand Down
6 changes: 6 additions & 0 deletions src/turbo_helper/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .middleware import get_current_request
from .response import HttpResponseSeeOther, TurboStreamResponse
from .shortcuts import redirect_303, respond_to
from .signals import after_create_commit, after_delete_commit, after_update_commit
from .stream import register_turbo_stream_action, turbo_stream
from .templatetags.turbo_helper import dom_id

Expand All @@ -14,4 +16,8 @@
"redirect_303",
"dom_id",
"respond_to",
"get_current_request",
"after_create_commit",
"after_update_commit",
"after_delete_commit",
]
76 changes: 0 additions & 76 deletions src/turbo_helper/channel_helper.py

This file was deleted.

Empty file.
72 changes: 72 additions & 0 deletions src/turbo_helper/channels/broadcasts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from actioncable import cable_broadcast
from django.template.loader import render_to_string

from turbo_helper.renderers import render_turbo_stream_refresh
from turbo_helper.stream import action_proxy

from .stream_name import stream_name_from


def broadcast_render_to(*streamables, **kwargs):
"""
Rails: Turbo::Streams::Broadcasts#broadcast_render_to
Help render Django template to Turbo Stream Channel
for example, in Django template, we subscribe to a Turbo stream Channel
{% turbo_stream_from 'chat' view.kwargs.chat_pk %}
Then in Python code
broadcast_render_to(
"chat",
instance.chat_id,
template="message_append.turbo_stream.html",
context={
"instance": instance,
},
)
"""
template = kwargs.pop("template", None)
broadcast_stream_to(
*streamables, content=render_to_string(template_name=template, **kwargs)
)


def broadcast_action_to(*streamables, action, target=None, targets=None, **kwargs):
"""
For now, we do not support:
broadcast_remove_to
broadcast_replace_to
broadcast_update_to
...
But we can use to do the same work
For example:
# remove DOM which has id="new_task"
broadcast_action_to("tasks", action="remove", target="new_task")
"""
content = action_proxy(
action,
target=target,
targets=targets,
**kwargs,
)
broadcast_stream_to(*streamables, content=content)


def broadcast_refresh_to(*streamables, request, **kwargs):
content = render_turbo_stream_refresh(request_id=request.turbo.request_id, **kwargs)
broadcast_stream_to(*streamables, content=content)


def broadcast_stream_to(*streamables, content):
stream_name = stream_name_from(*streamables)
cable_broadcast(
group_name=stream_name,
message=content,
)
39 changes: 39 additions & 0 deletions src/turbo_helper/channels/stream_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Tuple

from django.core import signing
from django.core.signing import Signer

from turbo_helper.templatetags.turbo_helper import dom_id

signer = Signer()


def stream_name_from(*streamables) -> str:
"""
Generate stream_name from a list of objects or a single object.
"""
if len(streamables) == 1:
return dom_id(streamables[0])
else:
return "_".join(stream_name_from(streamable) for streamable in streamables)


def generate_signed_stream_key(stream_name: str) -> str:
"""
Generate signed stream key from stream_name
"""
return signer.sign(stream_name)


def verify_signed_stream_key(signed_stream_key: str) -> Tuple[bool, str]:
"""
Verify signed stream key
"""
try:
unsigned_data = signer.unsign(signed_stream_key)
return True, unsigned_data

except signing.BadSignature:
pass

return False, ""
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from actioncable import ActionCableConsumer, CableChannel
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
from .stream_name import verify_signed_stream_key

signer = Signer()

Expand Down
44 changes: 40 additions & 4 deletions src/turbo_helper/middleware.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
import threading
from typing import Callable

from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject

from .constants import TURBO_STREAM_MIME_TYPE

_thread_locals = threading.local()


def get_current_request():
return getattr(_thread_locals, "request", None)


def set_current_request(request):
setattr(_thread_locals, "request", request) # noqa: B010


class SetCurrentRequest:
"""
Can let developer access Django request from anywhere
https://github.com/zsoldosp/django-currentuser
https://stackoverflow.com/questions/4716330/accessing-the-users-request-in-a-post-save-signal
"""

def __init__(self, request):
self.request = request

def __enter__(self):
set_current_request(self.request)

def __exit__(self, exc_type, exc_value, traceback):
# cleanup
set_current_request(None)


class TurboData:
def __init__(self, request: HttpRequest):
self.has_turbo_header = request.accepts(TURBO_STREAM_MIME_TYPE)
# be careful about the */* from browser
self.accept_turbo_stream = TURBO_STREAM_MIME_TYPE in request.headers.get(
"Accept", ""
)
self.frame = request.headers.get("Turbo-Frame", None)
self.request_id = request.headers.get("X-Turbo-Request-Id", None)

def __bool__(self):
"""
TODO: Deprecate
"""
return self.has_turbo_header
return self.accept_turbo_stream


class TurboMiddleware:
Expand All @@ -28,5 +62,7 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response

def __call__(self, request: HttpRequest) -> HttpResponse:
request.turbo = SimpleLazyObject(lambda: TurboData(request))
return self.get_response(request)
with SetCurrentRequest(request):
request.turbo = SimpleLazyObject(lambda: TurboData(request))
response = self.get_response(request)
return response
20 changes: 17 additions & 3 deletions src/turbo_helper/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

def render_turbo_stream(
action: str,
content: str,
content: Optional[str],
attributes: Dict[str, Any],
target: Optional[str] = None,
targets: Optional[str] = None,
Expand Down Expand Up @@ -71,8 +71,11 @@ def render_turbo_frame(frame_id: str, content: str, attributes: Dict[str, Any])


def render_turbo_stream_from(stream_name_array: List[Any]):
from .cable_channel import TurboStreamCableChannel
from .channel_helper import generate_signed_stream_key, stream_name_from
from turbo_helper.channels.stream_name import (
generate_signed_stream_key,
stream_name_from,
)
from turbo_helper.channels.streams_channel import TurboStreamCableChannel

stream_name_string = stream_name_from(*stream_name_array)

Expand All @@ -83,3 +86,14 @@ def render_turbo_stream_from(stream_name_array: List[Any]):
"channel": TurboStreamCableChannel.__name__,
}
return django_engine.from_string(template_string).render(context)


def render_turbo_stream_refresh(request_id, **attributes):
attributes["request-id"] = request_id
return render_turbo_stream(
action="refresh",
content=None,
target=None,
targets=None,
attributes=attributes,
)
12 changes: 9 additions & 3 deletions src/turbo_helper/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ def redirect_303(to: Union[str, Model], *args, **kwargs) -> HttpResponseSeeOther
def get_respond_to(request):
resp_format = ResponseFormat()

# TODO: move logic to ResponseFormat class
if request.accepts(TURBO_STREAM_MIME_TYPE):
accept_header = request.headers.get("Accept", "*/*")

# Most browsers send Accept: */* by default, so this would return True for all content types
# we do explicitly check here
if TURBO_STREAM_MIME_TYPE in accept_header:
resp_format.turbo_stream = True

if request.accepts("application/json"):
# Most browsers send Accept: */* by default, so this would return True for all content types
# we do explicitly check here
if "application/json" in accept_header:
resp_format.json = True

if request.accepts("text/html"):
# fallback
resp_format.html = True

return resp_format
Expand Down
Loading

0 comments on commit 158ef32

Please sign in to comment.