113 |
114 |
115 | ```
116 |
117 | Now run ``./manage.py shell``. Import the Turbo Stream and tell the stream to take the current timestamp and ``update`` the element with id `broadcast_box` on all subscribed pages.
118 |
119 | ```python
120 | from quickstart.streams import BroadcastStream
121 | from datetime import datetime
122 |
123 | BroadcastStream().update(text=f"The date and time is now: {datetime.now()}", id="broadcast_box")
124 | ```
125 |
126 | With the `quickstart/` path open in a browser window, watch as the broadcast pushes messages to the page.
127 |
128 | Now change `.update()` to `.append()` and resend the broadcast a few times. Notice you do not have to reload the page to get this modified behavior.
129 |
130 | Excited to learn more? Be sure to walk through the [tutorial](https://turbo-django.readthedocs.io/en/latest/index.html) and read more about what Turbo can do for you.
131 |
132 | ## Documentation
133 | Read the [full documentation](https://turbo-django.readthedocs.io/en/latest/index.html) at readthedocs.io.
134 |
135 |
136 | ## Contribute
137 |
138 | Discussions about a Django/Hotwire integration are happening on the [Hotwire forum](https://discuss.hotwired.dev/t/django-backend-support-for-hotwire/1570). And on Slack, which you can join by [clicking here!](https://join.slack.com/t/pragmaticmindsgruppe/shared_invite/zt-kl0e0plt-uXGQ1PUt5yRohLNYcVvhhQ)
139 |
140 | As this new magic is discovered, you can expect to see a few repositories with experiments and demos appear in [@hotwire-django](https://github.com/hotwire-django). If you too are experimenting, we encourage you to ask for write access to the GitHub organization and to publish your work in a @hotwire-django repository.
141 |
142 |
143 | ## License
144 |
145 | Turbo-Django is released under the [MIT License](https://opensource.org/licenses/MIT) to keep compatibility with the Hotwire project.
146 |
147 | If you submit a pull request. Remember to add yourself to `CONTRIBUTORS.md`!
148 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/doc/automake.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script detects changes in doc files and automatically triggers a rebuild.
4 |
5 | FOO=$(mktemp /tmp/turbodocs-automake.XXXXXX)
6 | while true; do
7 | for SRC in $(find . -name '*.rst' -mmin -1); do
8 | if [ "$SRC" -nt "$FOO" ]; then
9 | touch $FOO
10 | make clean html
11 | date
12 | break
13 | fi
14 | done
15 | sleep 1
16 | done
17 |
18 |
--------------------------------------------------------------------------------
/doc/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/doc/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx-autoapi==1.8.1
2 | sphinx-rtd-theme==0.5.1
3 | sphinxcontrib-fulltoc==1.2.0
4 | sphinx_toolbox==2.13.0
5 | -e git+https://github.com/hotwire-django/sphinx-hotwire-theme.git@main#egg=alabaster-hotwire
6 | jinja2==3.0.0
7 |
--------------------------------------------------------------------------------
/doc/source/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | div.document {
2 | width: 75%;
3 | }
4 | div.body {
5 | min-width: 450px;
6 | max-width: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/doc/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | # sys.path.insert(0, os.path.abspath('.'))
16 | import os
17 | import sys
18 |
19 | sys.path.insert(0, os.path.abspath("../"))
20 |
21 |
22 | # -- Project information -----------------------------------------------------
23 |
24 | project = 'turbo-django'
25 | copyright = '2022, Hotwire-Django Team'
26 | author = 'Hotwire-Django Team'
27 |
28 |
29 | # -- General configuration ---------------------------------------------------
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = [
35 | 'sphinxcontrib.fulltoc',
36 | 'sphinx.ext.autodoc',
37 | # 'autoapi.extension',
38 | 'sphinx_toolbox.code',
39 | ]
40 |
41 | # Add any paths that contain templates here, relative to this directory.
42 | templates_path = ['_templates']
43 |
44 | # List of patterns, relative to source directory, that match files and
45 | # directories to ignore when looking for source files.
46 | # This pattern also affects html_static_path and html_extra_path.
47 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'venv']
48 |
49 | # Document Python Code
50 | autoapi_type = "python"
51 | autoapi_dirs = ["../../turbo"]
52 | autoapi_ignore = ["*/tests/*.py"]
53 | autodoc_typehints = "description"
54 |
55 | # -- Options for HTML output -------------------------------------------------
56 |
57 | # The theme to use for HTML and HTML Help pages. See the documentation for
58 | # a list of builtin themes.
59 | #
60 | html_theme = 'alabaster_hotwire'
61 |
62 | # Add any paths that contain custom static files (such as style sheets) here,
63 | # relative to this directory. They are copied after the builtin static files,
64 | # so a file named "default.css" will overwrite the builtin "default.css".
65 | html_static_path = ['_static']
66 |
67 | html_context = {
68 | 'topbar': [
69 | {"url": "https://turbo-django.readthedocs.io/", "name": "Turbo Django", "active": True},
70 | {"url": "https://django-turbo-response.readthedocs.io/", "name": "Django Turbo Response"},
71 | {"name": "Stimulus Django"},
72 | ]
73 | }
74 |
75 | html_css_files = [
76 | 'css/custom.css',
77 | ]
78 |
--------------------------------------------------------------------------------
/doc/source/docutils.conf:
--------------------------------------------------------------------------------
1 | [restructuredtext parser]
2 | tab_width: 4
3 |
--------------------------------------------------------------------------------
/doc/source/index.rst:
--------------------------------------------------------------------------------
1 | .. warning::
2 | This library is unmaintained. Integrating Hotwire and Django is so easy
3 | that you are probably better served by writing a little bit of Python in your code
4 | than using a full blown library that adds another level of abstraction.
5 | It also seems that the Django community is leaning more towards HTMX than Hotwire
6 | so you might want to look over there if you want more "support"
7 | (but we still think that Hotwire is very well suited to be used with Django)
8 |
9 |
10 |
11 | Unmaintained // Turbo Django
12 | ============
13 |
14 | Turbo Django is a project that integrates the `Hotwire Turbo framework `_ with `Django `_, allowing for rendered page updates to be delivered live, over the wire. By keeping template rendering in Django, dynamic and interactive web pages can be written without any serialization frameworks or JavaScript, dramatically simplifying development.
15 |
16 | Topics
17 | ------
18 |
19 | .. toctree::
20 | :maxdepth: 2
21 |
22 | installation
23 | topics/quickstart.rst
24 | tutorial/index
25 | topics/turbo.rst
26 | topics/streams.rst
27 | topics/model_stream.rst
28 | topics/components.rst
29 | topics/templates.rst
30 |
31 |
32 | Reference
33 | ---------
34 |
35 | .. toctree::
36 | :maxdepth: 1
37 |
38 | GitHub Repo
39 |
--------------------------------------------------------------------------------
/doc/source/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | ============
5 | Requirements
6 | ============
7 |
8 | This library is tested for Python 3.8+ and Django 3.1+
9 |
10 | ============
11 | Installation
12 | ============
13 |
14 |
15 | Turbo Django is available on PyPI - to install it, just run:
16 |
17 | .. code-block:: sh
18 |
19 | pip install turbo-django
20 |
21 |
22 | .. note::
23 |
24 | Both Turbo and Turbo Django are under beta development and the API can change quickly as new features are added and the API is refined. It would be prudent to pin to a specific version until the first major release. You can pin with pip using a command like ``pip install turbo-django==0.3.0``. The latest version can be found `at PyPi `_.
25 |
26 | Once that's done, you should add ``turbo`` and ``channels`` to your
27 | ``INSTALLED_APPS`` setting:
28 |
29 | .. code-block:: python
30 |
31 | INSTALLED_APPS = (
32 | 'django.contrib.auth',
33 | 'django.contrib.contenttypes',
34 | 'django.contrib.sessions',
35 | 'django.contrib.sites',
36 | ...
37 | 'turbo',
38 | 'channels',
39 | )
40 |
41 | CHANNEL_LAYERS = {
42 | # You will need to `pip install channels_redis` and configure a redis instance.
43 | # Using InMemoryChannelLayer will not work as the stored memory is not shared between threads.
44 | # See https://channels.readthedocs.io/en/latest/topics/channel_layers.html
45 | "default": {
46 | "BACKEND": "channels_redis.core.RedisChannelLayer",
47 | "CONFIG": {
48 | "hosts": [("127.0.0.1", 6379)],
49 | },
50 | },
51 | }
52 |
53 |
54 | .. note::
55 | Turbo relies on the ``channels`` library to push data to the client (also known as Turbo Streams). Adding channels may not be needed if using only implementing Turbo Frames to component-ify your app. For the tutorial, you will need channels installed.
56 |
57 |
58 | Then, adjust your project's ``asgi.py`` to wrap the Django ASGI application::
59 |
60 | import os
61 |
62 | from django.core.asgi import get_asgi_application
63 | from channels.routing import ProtocolTypeRouter
64 | from channels.auth import AuthMiddlewareStack
65 | from turbo.consumers import TurboStreamsConsumer
66 |
67 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
68 |
69 |
70 | application = ProtocolTypeRouter({
71 | "http": get_asgi_application(),
72 | "websocket": AuthMiddlewareStack(TurboStreamsConsumer.as_asgi()),
73 | })
74 |
75 | And finally, set your ``ASGI_APPLICATION`` setting to point to that routing
76 | object as your root application:
77 |
78 | .. code-block:: python
79 |
80 | ASGI_APPLICATION = "myproject.asgi.application"
81 |
82 | All set! ``turbo`` is now ready to use in your Django app.
83 |
--------------------------------------------------------------------------------
/doc/source/topics/components.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | Components
3 | ==========
4 |
5 |
6 | Components are a subclass of `Stream` that simplifies implementation of streams.
7 |
8 |
9 |
10 | Creating a Component
11 | ====================
12 |
13 | Components are a type of stream with a template. As components are a type of stream, components must be either be created in or imported to `streams.py` to be registered.
14 |
15 |
16 | Quick example
17 | -------------
18 |
19 | .. code-block:: python
20 | :caption: app/streams.py
21 |
22 | from turbo.components import BroadcastComponent
23 |
24 | class AlertBroadcastComponent(BroadcastComponent):
25 |
26 | template_name = "components/sample_broadcast_component.html"
27 |
28 |
29 | Add a simple template:
30 |
31 | .. code-block:: html
32 | :caption: templates/components/sample_broadcast_component.html
33 |
34 | {% if alert_content %}
35 |
36 | {{alert_content}}
37 |
38 | {% endif %}
39 |
40 | The component can be rendered in one of two ways.
41 |
42 | .. code-block:: html
43 | :caption: templates/view_template.html
44 |
45 | {% load turbo_streams %}
46 |
47 | If an instance of the component is passed in via the view:
48 | {% turbo_component alert_component %}
49 |
50 | To access the component globally:
51 | {% turbo_component "app_name:AlertBroadcastComponent" %}
52 |
53 |
54 | This will insert the contents of `components/sample_broadcast_component.html` on the template.
55 |
56 | To stream updated content to the view, open a python terminal, instanciate the component, and render a new update.
57 |
58 |
59 | .. code-block:: python
60 |
61 | from app_name.streams import AlertBroadcastComponent
62 | alert_component = AlertBroadcastComponent()
63 | alert_component.render(
64 | alert_class='warning',
65 | alert_content='The server will restart in 10 minutes'
66 | )
67 |
68 |
69 | .. admonition:: Multiple identical components
70 |
71 | Using the same component twice in a template, will only update the first component on the page. This is a current purposeful limitation of the Hotwire framework. In the above examples, while the components will render initially, only the first component will receive the streamed content.
72 |
73 |
74 |
75 |
76 | Full example
77 | ------------
78 |
79 | .. code-block:: python
80 | :caption: app/streams.py
81 |
82 | from turbo.components import BroadcastComponent
83 |
84 | class AlertBroadcastComponent(BroadcastComponent):
85 |
86 | template_name = "components/sample_broadcast_component.html"
87 |
88 | def get_context(self):
89 | """
90 | Return the default context to render a component.
91 | """
92 | return {}
93 |
94 | def user_passes_test(self, user):
95 | """
96 | Only allow access to the component stream if the user passes
97 | this test.
98 | """
99 | return user.is_authenticated
100 |
101 |
102 | .. module:: turbo.components
103 |
104 | BroadcastComponent
105 | ==================
106 |
107 | .. class:: BroadcastComponent
108 |
109 | A broadcast component will stream a template to all users.
110 |
111 | Example
112 | -------
113 |
114 | .. code-block:: python
115 | :caption: app/streams.py
116 |
117 | from turbo.components import BroadcastComponent
118 |
119 | class AlertBroadcastComponent(BroadcastComponent):
120 | template_name = "components/sample_broadcast_component.html"
121 |
122 | .. code-block:: html
123 | :caption: templates/components/sample_broadcast_component.html
124 |
125 | {% if alert_content %}
126 |
127 | {{alert_content}}
128 |
129 | {% endif %}
130 |
131 | .. code-block:: html
132 | :caption: templates/view_template.html
133 |
134 | {% load turbo_streams %}
135 |
136 | {% turbo_component "app_name:AlertBroadcastComponent" %}
137 |
138 |
139 |
140 | To stream an updated template to the component:
141 |
142 | .. code-block:: python
143 |
144 | from .streams import AlertBroadcastComponent
145 |
146 | component = AlertBroadcastComponent()
147 | component.render(
148 | alert_class='warning',
149 | alert_content='The server will restart in 10 minutes'
150 | )
151 |
152 |
153 | UserBroadcastComponent
154 | ======================
155 |
156 | .. class:: UserBroadcastComponent
157 |
158 | A user broadcast component will stream a template to a specific user.
159 |
160 | Example
161 | -------
162 |
163 | .. code-block:: python
164 | :caption: app/streams.py
165 |
166 | from turbo.components import UserBroadcastComponent
167 |
168 | class CartCountComponent(UserBroadcastComponent):
169 | template_name = "components/cart_count_component.html"
170 |
171 | def get_context(self):
172 | return {
173 | "count": self.user.cart.items_in_cart
174 | }
175 |
176 |
177 | .. code-block:: html
178 | :caption: templates/components/cart_count_component.html
179 |
180 |
191 |
192 |
193 | .. code-block:: html
194 | :caption: templates/view_template.html
195 |
196 | {% load turbo_streams %}
197 |
198 | {% turbo_component "chat:CartCountComponent" request.user %}
199 | or
200 | {% turbo_component cart_count_component %}
201 |
202 |
203 |
--------------------------------------------------------------------------------
/doc/source/topics/model_stream.rst:
--------------------------------------------------------------------------------
1 | =================
2 | ModelStream
3 | =================
4 |
5 |
6 | A common reason to stream data is to send page updates when a model is created, modified, or deleted. To organize these events in one place, Turbo Django uses ``ModelStream``. These classes will trigger the code to run when a model instance is saved and deleted. ``ModelStream`` objects are declared and automatically detected in ``streams.py``.
7 |
8 | When a ModelStream is registered to a model, the model instance will automatically gain a `.stream` attribute that references the stream. For this reason, only one model stream can be attached to each model.
9 |
10 | .. admonition:: Primary Key Needed
11 |
12 | You can only broadcast to instances that have a primary key. A ``ValueError`` is thrown when trying to broadcast to an object that does not have a primary key set.
13 |
14 |
15 | Example
16 | ----------------------
17 |
18 | The following demonstrates a sample implementation of ModelStreams for a chat application. In this examples, a user would subscribe to a Room, however, the messages are the items being added and removed. A stream is created for both models - giving them both `.stream` attributes. When the message is saved, the message then references it's parent room stream, and either appends or replaces the chat message if it was created or modified. If the message is deleted, the parent room stream is notified to remove the message block with the provided id.
19 |
20 | .. code-block:: python
21 | :caption: app/streams.py
22 |
23 | from .models import Message, Room
24 |
25 | import turbo
26 |
27 | class RoomStream(turbo.ModelStream):
28 |
29 | class Meta:
30 | model = Room
31 |
32 |
33 |
34 | class MessageStream(turbo.ModelStream):
35 |
36 | class Meta:
37 | model = Message
38 |
39 | def on_save(self, message, created, *args, **kwargs):
40 | if created:
41 | message.room.stream.append("chat/message.html", {"message": message}, id="messages")
42 | else:
43 | message.room.stream.replace("chat/message.html", {"message": message}, id=f"message-{message.id}")
44 |
45 | def on_delete(self, message, *args, **kwargs):
46 | message.room.stream.remove(id=f"message-{message.id}")
47 |
48 | def user_passes_test(self, user):
49 | # if user.can_access_message(self.pk):
50 | # return True
51 | return True
52 |
--------------------------------------------------------------------------------
/doc/source/topics/quickstart.rst:
--------------------------------------------------------------------------------
1 | .. warning::
2 | This library is unmaintained. Integrating Hotwire and Django is so easy
3 | that you are probably better served by writing a little bit of Python in your code
4 | than using a full blown library that adds another level of abstraction.
5 | It also seems that the Django community is leaning more towards HTMX than Hotwire
6 | so you might want to look over there if you want more "support"
7 | (but we still think that Hotwire is very well suited to be used with Django)
8 |
9 | ==========
10 | Unmaintained//Quickstart
11 | ==========
12 |
13 | Want to see Turbo in action? Here's a simple broadcast that can be setup in less than a minute.
14 |
15 | **The basics:**
16 |
17 | * A Turbo Stream class is declared in python.
18 |
19 | * A template subscribes to the Turbo Stream.
20 |
21 | * HTML is be pushed to all subscribed pages which replaces the content of specified HTML p tag.
22 |
23 |
24 | Example
25 | =============
26 |
27 | First, declare the Stream.
28 |
29 | .. code-block:: python
30 | :caption: streams.py
31 |
32 | import turbo
33 |
34 | class BroadcastStream(turbo.Stream):
35 | pass
36 |
37 |
38 | Then, create a template that subscribes to the stream.
39 |
40 | .. code-block:: python
41 | :caption: urls.py
42 |
43 | from django.urls import path
44 | from django.views.generic import TemplateView
45 |
46 | urlpatterns = [
47 | path('', TemplateView.as_view(template_name='broadcast_example.html'))
48 | ]
49 |
50 |
51 | .. code-block:: html
52 | :caption: broadcast_example.html
53 |
54 | {% load turbo_streams %}
55 |
56 |
57 |
58 | {% include "turbo/head.html" %}
59 |
60 |
61 | {% turbo_subscribe 'quickstart:BroadcastStream' %}
62 |
63 |
Placeholder for broadcast
64 |
65 |
66 |
67 | .. note::
68 | Broadcasts can target any HTML element on a page subscribed to its stream. Target elements do not need be wrapped in any ``turbo`` style tag.
69 |
70 |
71 | Now open ``./manage.py shell``. Import the Turbo Stream and tell the stream to take the current timestamp and ``update`` the element with id `broadcast_box` on all subscribed pages.
72 |
73 | .. code-block:: python
74 |
75 | from quickstart.streams import BroadcastStream
76 | from datetime import datetime
77 |
78 | BroadcastStream().update(text=f"{datetime.now()}: This is a broadcast.", id="broadcast_box")
79 |
80 | With the ``quickstart/`` path open in a browser window, watch as the broadcast pushes messages to the page.
81 |
82 | Now change ``.update()`` to ``.append()`` and resend the broadcast a few times. Notice you do not have to reload the page to get this modified behavior.
83 |
84 | Excited to learn more? Be sure to walk through the :doc:`tutorial ` and read more about what the :doc:`Turbo ` class can do.
85 |
--------------------------------------------------------------------------------
/doc/source/topics/streams.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Streams
3 | =======
4 |
5 | Streams allow data to be sent to a currently loaded page. Stream classes must be explicitly declared in `streams.py` and should contain all render and positioning logic. Stream classes are also used to add permissions.
6 |
7 |
8 | Example
9 | ----------------------
10 |
11 | .. code-block:: python
12 | :caption: app/streams.py
13 |
14 | from .models import Message, Room
15 |
16 | import turbo
17 |
18 | class BroadcastStream(turbo.Stream):
19 |
20 | def send_message(self, message):
21 | # This is a user-defined method that encapsulates render and positioning logic.
22 | # It would be called from code using BroadcastStream().send_message("test message")
23 | self.update(text=message, id="broadcast_box")
24 |
25 | def user_passes_test(self, user):
26 | # user_passes_test is a built-in method that is extended to add permissions to streams.
27 | return True
28 |
29 |
30 | .. module:: turbo.Stream
31 |
32 |
33 | .. method:: append(template=None, context=None, text=None, selector=None, id=None)
34 |
35 | Add the rendered template to the end of the specified HTML element.
36 |
37 | .. method:: prepend(template=None, context=None, text=None, selector=None, id=None)
38 |
39 | Add the rendered template to the beginning of the specified HTML element.
40 |
41 | .. method:: replace(template=None, context=None, text=None, selector=None, id=None)
42 |
43 | Remove and replace the specified HTML element with the rendered template.
44 |
45 | .. method:: update(template=None, context=None, text=None, selector=None, id=None)
46 |
47 | Replace the contents inside the specified HTML element with the rendered template.
48 |
49 | .. method:: before(template=None, context=None, text=None, selector=None, id=None)
50 |
51 | Insert the rendered template before the specified HTML element.
52 |
53 | .. method:: after(template=None, context=None, text=None, selector=None, id=None)
54 |
55 | Insert the template after the specified HTML element.
56 |
57 | .. method:: remove(selector=None, id=None)
58 |
59 | Remove the given HTML element. The rendered template will not be used. As no template is used to remove divs, this can also be called directly from the shortcut ``remove_frame()``. Ex: ``remove_frame(id='div_to_remove')``
60 |
61 | .. method:: stream(frame: "TurboRender")
62 |
63 | Send a :doc:`TurboRender ` object to this stream.
64 |
65 | .. method:: stream_raw(raw_text: str)
66 |
67 | Send raw text to this stream. This will not be prewrapped in a turbo stream tag as it would be in `stream()`
68 |
69 | .. method:: user_passes_test(user) -> bool
70 |
71 | Return True if a user has permission to access this stream. If False, the websocket connection will be rejected. When creating a stream, extend this method to exclude certain users from resources.
72 |
73 |
74 |
--------------------------------------------------------------------------------
/doc/source/topics/templates.rst:
--------------------------------------------------------------------------------
1 | Templates
2 | ==========
3 |
4 | Templates subscribe to streams using the ``turbo_subscribe`` template tag. Import this tag by calling ``{% load turbo_streams %}``. Pass a channel object, name-spaced channel name as a string, or pass a Django instance to listen to messages sent to a particular object. This tag can be called anywhere on the page and can be called multiple times if desired.
5 |
6 | .. code-block:: html
7 | :caption: broadcast_example.html
8 |
9 |
10 | {% load turbo_streams %}
11 |
12 |
13 | {% turbo_subscribe RoomListChannel %}
14 | {% turbo_subscribe 'chat:RoomListChannel' %}
15 | {% turbo_subscribe room %}
16 |
17 |
18 | {% turbo_subscribe 'chat:RoomListChannel' room %}
19 |
20 |
21 |
22 |
23 | It is now possible to send and place html to the subscribed page using the following:
24 |
25 | .. code-block:: python
26 |
27 | from turbo import Turbo
28 |
29 | # Send to a standard Channel
30 | RoomListChannel.replace(
31 | "alert.html",
32 | {'message': 'Server restart in 1 minute.'},
33 | id='alert_div'
34 | )
35 |
36 |
37 | # Send to a ModelStream
38 | room = Room.objects.first()
39 |
40 | room.channel.append(
41 | "new_message.html",
42 | {'message': 'New message'}
43 | id='messages_container'
44 | )
45 |
46 |
47 |
48 |
49 | ``turbo_subscribe tag``
50 | -----------------------
51 |
52 | Tells the page to subscribe to the channel or instance.
53 |
54 | Example usage::
55 |
56 | {% load turbo_streams %}
57 | {% turbo_subscribe 'chat:BroadcastChannel' %}
58 |
59 | Stream names can be strings or generated from instances::
60 |
61 | {% turbo_subscribe room %}
62 |
63 | Listen to multiple streams by adding additional arguments::
64 |
65 | {% turbo_subscribe 'chat:BroadcastChannel' room %}
66 |
67 |
--------------------------------------------------------------------------------
/doc/source/topics/turbo.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Turbo Frames
3 | =============
4 |
5 | **** Turbo Frames allow parts of the page to be updated on request. Each turbo-frame must have an id that is shared between the parent frame, and the elements that will be loaded into the frame.
6 |
7 | .. note::
8 | Be sure to read the `official documentation of Turbo Frames `_.
9 |
10 |
11 |
12 | .. module:: turbo.Turbo
13 |
14 | Turbo Frames can be rendered in python using convience methods.
15 |
16 | .. module:: turbo.shortcuts
17 |
18 | .. method:: render_frame(request, template_name: str, context=None) -> TurboRender
19 |
20 | .. method:: render_frame_string(text: str) -> TurboRender
21 |
22 | .. method:: remove_frame(selector=None, id=None) -> TurboRender
23 |
24 | Create a TurboRender object that removes a frame. Since there is no content to be inserted, no template or text is passed. Instead,
25 |
26 | .. code-block:: python
27 |
28 | from turbo.shortcuts import render_frame, remove_frame
29 |
30 | def post(self, request, *args, **kwargs):
31 |
32 | form = RoomForm(request.POST)
33 | if form.is_valid():
34 | form.save()
35 |
36 | new_form = RoomForm()
37 |
38 | return (
39 | render_frame(
40 | request,
41 | "chat/components/create_room_form.html",
42 | {"form": new_form},
43 | )
44 | .replace(id="create-room-form")
45 | .response
46 | )
47 |
48 |
49 |
50 | TurboRender methods
51 | ===================
52 |
53 | .. module:: turbo.TurboRender
54 |
55 | Once a turbo frame has been rendered, it needs to know where to position itself. The following methods let the client page know where to position the new content when it is received.
56 |
57 | The typical use is to chain ``render`` and ``.`` commands into one logical, easy-to-read statement.
58 |
59 | .. code-block:: python
60 |
61 | render_frame(
62 | request, 'broadcast.html', {'content': "New message!"}
63 | ).update(".alert_box")
64 |
65 | Each of the following methods take either an ``selector`` or ``id`` keyword argument to specify which HTML element will receive the action. ``selector`` is the first argument, so no keyword specifier is needed.
66 |
67 |
68 | .. method:: append(selector=None, id=None)
69 |
70 | Add the rendered template to the end of the specified HTML element.
71 |
72 | .. method:: prepend(selector=None, id=None)
73 |
74 | Add the rendered template to the beginning of the specified HTML element.
75 |
76 | .. method:: replace(selector=None, id=None)
77 |
78 | Remove and replace the specified HTML element with the rendered template.
79 |
80 | .. method:: update(selector=None, id=None)
81 |
82 | Replace the contents inside the specified HTML element with the rendered template.
83 |
84 | .. method:: remove(selector=None, id=None)
85 |
86 | Remove the given HTML element. The rendered template will not be used. As no template is used to remove divs, this can also be called directly from the shortcut ``remove_frame()``. Ex: ``remove_frame(id='div_to_remove')``
87 |
88 | .. method:: before(selector=None, id=None)
89 |
90 | Insert the rendered template before the specified HTML element.
91 |
92 | .. method:: after(selector=None, id=None)
93 |
94 | Insert the template after the specified HTML element.
95 |
96 | .. method:: response
97 |
98 | Property. Return this rendered template as an HttpResponse with a "text/vnd.turbo-stream.html" content type. This allows for turbo-stream elements to be returned from a form submission. See the Turbo documentation for more detail (https://turbo.hotwired.dev/handbook/drive#streaming-after-a-form-submission)
99 |
100 | .. code-block:: python
101 |
102 | frame = render_frame(
103 | request, "reminders/reminder_list_item.html", {'reminder': reminder}
104 | ).append(id='reminders')
105 | return frame.response
106 |
107 |
--------------------------------------------------------------------------------
/doc/source/tutorial/index.rst:
--------------------------------------------------------------------------------
1 | .. warning::
2 | This library is unmaintained. Integrating Hotwire and Django is so easy
3 | that you are probably better served by writing a little bit of Python in your code
4 | than using a full blown library that adds another level of abstraction.
5 | It also seems that the Django community is leaning more towards HTMX than Hotwire
6 | so you might want to look over there if you want more "support"
7 | (but we still think that Hotwire is very well suited to be used with Django)
8 |
9 |
10 | Unmaintained//Tutorial
11 | ========
12 |
13 | Turbo-Django allows you to easily integrate the Hotwire Turbo framework into your
14 | Django site. This will allow clients to receive blocks of html sent from your web server
15 | without using HTTP long-polling or other expensive techniques. This makes for
16 | dynamic interactive webpages, without all the mucking about with serializers and JavaScript.
17 |
18 | In this tutorial we will build a simple chat server, where you can join an
19 | online room, post messages to the room, and have others in the same room see
20 | those messages immediately.
21 |
22 | .. toctree::
23 | :maxdepth: 1
24 |
25 | part_1
26 | part_2
27 | part_3
28 | part_4
29 | part_5
30 |
--------------------------------------------------------------------------------
/doc/source/tutorial/part_1.rst:
--------------------------------------------------------------------------------
1 | =============================================
2 | Part 1 - Setup Project
3 | =============================================
4 |
5 | In this tutorial we will build a simple chat server. It will consist of two pages:
6 |
7 | * The room list - consisting of the list of all available chat rooms.
8 | * The chat room - where anyone can go and post a message.
9 |
10 | This tutorial assumes basic knowledge of the Django framework and will use class-based generic views to minimize the amount of code and to focus more on Hotwire.
11 |
12 | Create a virtual environment using a tool of your choice and install ``turbo-django``, along with ``django``, and ``channels``.
13 |
14 | .. code-block:: shell
15 |
16 | $ pip install django turbo-django channels channels_redis
17 |
18 |
19 | Start a django project and create an app called ``chat``
20 |
21 | .. code-block:: shell
22 |
23 | $ django-admin startproject turbotutorial
24 | $ cd turbotutorial/
25 | $ ./manage.py startapp chat
26 |
27 | You should now have a set of directories that looks something like:
28 |
29 | .. code-block:: shell
30 |
31 | turbotutorial/
32 | chat/
33 | migrations/
34 | admin.py
35 | apps.py
36 | models.py
37 | tests.py
38 | views.py
39 | turbotutorial/
40 | asgi.py
41 | settings.py
42 | urls.py
43 | wsgi.py
44 |
45 |
46 | Open ``turbotutorial/settings.py``.
47 |
48 | * Add ``turbo``, ``channels``, and ``chat`` to ``INSTALLED_APPS``.
49 | * Change ``WSGI_APPLICATION = 'turbotutorial.wsgi.application'`` to ``ASGI_APPLICATION = 'turbotutorial.asgi.application'``
50 |
51 | Your ``settings.py`` file should now look like this.
52 |
53 | .. code-block:: python
54 |
55 | ASGI_APPLICATION = 'turbotutorial.asgi.application'
56 |
57 |
58 | INSTALLED_APPS = [
59 | 'django.contrib.admin',
60 | 'django.contrib.auth',
61 | 'django.contrib.contenttypes',
62 | 'django.contrib.sessions',
63 | 'django.contrib.messages',
64 | 'django.contrib.staticfiles',
65 | 'turbo',
66 | 'channels',
67 | 'chat'
68 | ]
69 |
70 | CHANNEL_LAYERS = {
71 | "default": {
72 | "BACKEND": "channels_redis.core.RedisChannelLayer",
73 | "CONFIG": {
74 | "hosts": [("127.0.0.1", 6379)], # Set to your local redis host
75 | },
76 | },
77 | }
78 |
79 |
80 |
81 | You should now be able to run ``python manage.py runserver``, visit ``http://127.0.0.1:8000/`` and see the standard django startup screen greeting: `The install worked successfully! Congratulations!`. If so, we're ready to :doc:`start coding `.
82 |
--------------------------------------------------------------------------------
/doc/source/tutorial/part_2.rst:
--------------------------------------------------------------------------------
1 | ==============================================
2 | Part 2 - Models, Views, and Templates
3 | ==============================================
4 |
5 | Begin by building out the models, views and templates used in the chat application. Nothing is this section is Turbo-specific - that will be introduced in :doc:`the next section `.
6 |
7 | Models
8 | ==============
9 |
10 | This chat application will be set up with two simple models: ``Rooms`` and ``Messages``. Start by creating the models with in ``chat/models.py``
11 |
12 | .. code-block:: python
13 | :caption: chat/models.py
14 |
15 | from django.db import models
16 |
17 | class Room(models.Model):
18 | name = models.CharField(max_length=255)
19 |
20 | class Message(models.Model):
21 |
22 | room = models.ForeignKey(Room, related_name="messages", on_delete=models.CASCADE)
23 | text = models.CharField(max_length=255)
24 | created_at = models.DateTimeField(auto_now_add=True)
25 |
26 |
27 |
28 | Make a migration and migrate, and then create a test room.
29 |
30 | .. code-block:: shell
31 |
32 | ./manage.py makemigrations
33 | ./manage.py migrate
34 | ./manage.py shell
35 |
36 |
37 | .. code-block:: python
38 |
39 | >>> from chat.models import Room
40 | >>> Room.objects.create(name="Test Room")
41 |
42 | >>> exit()
43 |
44 |
45 | Views and URLs
46 | ================================
47 |
48 | This tutorial uses generic class-based views to keep the tutorial concise. Add generic `List`, `Detail`, and `Update` views to ``chat/views.py``, and the urls to access them. There is nothing turbo-specific in the following section - we'll be adding that next.
49 |
50 | .. code-block:: python
51 | :caption: chat/views.py
52 |
53 | from django.shortcuts import render, reverse, get_object_or_404
54 |
55 | from django.views.generic import CreateView, ListView, DetailView
56 |
57 | from chat.models import Room, Message
58 |
59 | class RoomList(ListView):
60 | model = Room
61 | context_object_name = "rooms"
62 |
63 |
64 | class RoomDetail(DetailView):
65 | model = Room
66 | context_object_name = "room"
67 |
68 |
69 | class MessageCreate(CreateView):
70 | model = Message
71 | fields = ["text"]
72 | template_name = "chat/components/send_message_form.html"
73 |
74 | def get_success_url(self):
75 | # Redirect to the empty form
76 | return reverse("message_create", kwargs={"pk": self.kwargs["pk"]})
77 |
78 | def form_valid(self, form):
79 | room = get_object_or_404(Room, pk=self.kwargs["pk"])
80 | form.instance.room = room
81 | return super().form_valid(form)
82 |
83 |
84 | .. code-block:: python
85 | :caption: turbotutorial/urls.py
86 |
87 | from chat import views
88 |
89 | urlpatterns = [
90 | path("", views.RoomList.as_view(), name="index"),
91 | path("/", views.RoomDetail.as_view(), name="room_detail"),
92 | path("/message_create", views.MessageCreate.as_view(), name="message_create"),
93 | ]
94 |
95 |
96 | Templates
97 | =========
98 |
99 | Finally, create the templates for the generic views.
100 |
101 | .. code-block:: html
102 | :caption: turbotutorial/chat/templates/room_list.html
103 |
104 |
105 |
106 |
107 |
108 | Chat Rooms
109 |
110 |
111 |
138 | {% for message in room.messages.all %}
139 |
{{message.created_at}}: {{message.text}}
140 | {% endfor %}
141 |
142 |
143 |
144 |
145 |
146 | .. code-block:: html
147 | :caption: turbotutorial/chat/templates/room_form.html
148 |
149 |
154 |
155 | Test in your browser to ensure each of the views correctly load. You should be able to get to the `Test Room` detail page from the room list. This application will now display all rooms and messages for each room, but a page refresh is required to see changes. It is time to spice things up and add :doc:`some interactivity ` to this basic app.
156 |
157 |
158 |
--------------------------------------------------------------------------------
/doc/source/tutorial/part_3.rst:
--------------------------------------------------------------------------------
1 | ===============================
2 | Part 3 - Your First Turbo Frame
3 | ===============================
4 |
5 | Listen to Turbo Streams
6 | =========================
7 |
8 | It's time to start creating a dynamic, interactive application. Start by getting Django to listen to websockets by modifying ``asgi.py`` to the following:
9 |
10 | .. code-block:: python
11 | :caption: turbodjango/asgi.py
12 |
13 | import os
14 |
15 | from django.core.asgi import get_asgi_application
16 | from channels.routing import ProtocolTypeRouter
17 | from turbo.consumers import TurboStreamsConsumer
18 |
19 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'turbodjango.settings')
20 |
21 |
22 | application = ProtocolTypeRouter({
23 | "http": get_asgi_application(),
24 | "websocket": TurboStreamsConsumer.as_asgi()
25 | })
26 |
27 |
28 |
29 | A Component-Based Mindset
30 | =========================
31 |
32 | Like many modern JavaScript-based frameworks, it is helpful to start thinking of the webpage as constructed of components. This means breaking down templates into sub-templates with one specific function that are used as the building blocks for each page.
33 |
34 | With that in mind, let's make a `components/` directory for these sub-templates and start work on our first component - a form to create a message in the chat room.
35 |
36 |
37 | .. code-block:: html
38 | :caption: templates/chat/room_detail.html
39 |
40 |
41 |
42 | Room Detail
43 | {% include "turbo/head.html" %}
44 |
45 |
46 | ...
47 |
48 |
49 |