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

rename file #41

Merged
merged 8 commits into from
Jan 24, 2024
Merged
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
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