Skip to content

Commit 2e7dcf6

Browse files
authored
Utilize django include for HTTP URLs (#45)
- fix #37 - Follows Django conventions for HTTP URLs. Allows us to add more in the future when needed. - Major overhaul of the readme to improve readability. - Expose IdomWebsocket to encourage type hinting - Removes IDOM_BASE_URL as it was difficult to use in conjunction with Django base URLs
1 parent d3aee5e commit 2e7dcf6

File tree

13 files changed

+113
-185
lines changed

13 files changed

+113
-185
lines changed

README.md

+70-135
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
1-
# Django IDOM
2-
3-
<p>
4-
<a href="https://github.com/idom-team/django-idom/actions?query=workflow%3ATest">
5-
<img alt="Tests" src="https://github.com/idom-team/django-idom/workflows/Test/badge.svg?event=push" />
6-
</a>
7-
<a href="https://pypi.python.org/pypi/django-idom">
8-
<img alt="Version Info" src="https://img.shields.io/pypi/v/django-idom.svg"/>
9-
</a>
10-
<a href="https://github.com/idom-team/django-idom/blob/main/LICENSE">
11-
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-purple.svg">
12-
</a>
13-
</p>
14-
15-
`django-idom` allows Django to integrate with [IDOM](https://github.com/idom-team/idom),
16-
a package inspired by [ReactJS](https://reactjs.org/) for creating responsive web
17-
interfaces in pure Python.
1+
# Django IDOM &middot; [![Tests](https://github.com/idom-team/django-idom/workflows/Test/badge.svg?event=push)](https://github.com/idom-team/django-idom/actions?query=workflow%3ATest) [![PyPI Version](https://img.shields.io/pypi/v/django-idom.svg)](https://pypi.python.org/pypi/django-idom) [![License](https://img.shields.io/badge/License-MIT-purple.svg)](https://github.com/idom-team/django-idom/blob/main/LICENSE)
2+
3+
`django-idom` allows Django to integrate with [IDOM](https://github.com/idom-team/idom), a reactive Python web framework for building **interactive websites without needing a single line of Javascript**.
184

195
**You can try IDOM now in a Jupyter Notebook:**
206
<a
@@ -27,70 +13,60 @@ interfaces in pure Python.
2713
src="https://mybinder.org/badge_logo.svg"/>
2814
</a>
2915

30-
# Install Django IDOM
16+
# Quick Example
3117

32-
```bash
33-
pip install django-idom
34-
```
18+
## `example_app/components.py`
19+
20+
This is where you'll define your [IDOM](https://github.com/idom-team/idom) components. Ultimately though, you should
21+
feel free to organize your component modules you wish. Any components created will ultimately be referenced
22+
by Python dotted path in `your-template.html`.
3523

36-
# Django Integration
24+
```python
25+
from idom import component, html
26+
from django_idom import IdomWebsocket
3727

38-
To integrate IDOM into your application you'll need to modify or add the following files to `your_project`:
3928

40-
```
41-
your_project/
42-
├── __init__.py
43-
├── asgi.py
44-
├── settings.py
45-
├── urls.py
46-
└── example_app/
47-
├── __init__.py
48-
├── components.py
49-
├── templates/
50-
│ └── your-template.html
51-
└── urls.py
29+
@component
30+
def Hello(websocket: IdomWebsocket, greeting_recipient: str): # Names are CamelCase by ReactJS convention
31+
return html.header(f"Hello {greeting_recipient}!")
5232
```
5333

54-
## `asgi.py`
34+
## [`example_app/templates/your-template.html`](https://docs.djangoproject.com/en/dev/topics/templates/)
5535

56-
Follow the [`channels`](https://channels.readthedocs.io/en/stable/)
57-
[installation guide](https://channels.readthedocs.io/en/stable/installation.html) in
58-
order to create ASGI websockets within Django. Then, we will add a path for IDOM's
59-
websocket consumer using `IDOM_WEBSOCKET_PATH`.
36+
In your templates, you may add IDOM components into your HTML by using the `idom_component`
37+
template tag. This tag requires the dotted path to the component function. Additonally, you can
38+
pass in keyworded arguments into your component function.
6039

61-
_Note: If you wish to change the route where this websocket is served from, see the
62-
available [settings](#settingspy)._
40+
In context this will look a bit like the following...
6341

64-
```python
42+
```jinja
43+
{% load idom %}
6544
66-
import os
45+
<!DOCTYPE html>
46+
<html>
47+
<body>
48+
...
49+
{% idom_component "my_django_project.example_app.components.Hello" greeting_recipient="World" %}
50+
</body>
51+
</html>
52+
```
6753

68-
from django.core.asgi import get_asgi_application
54+
# Installation
6955

70-
from django_idom import IDOM_WEBSOCKET_PATH
56+
Install `django-idom` via pip.
7157

72-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
58+
```bash
59+
pip install django-idom
60+
```
7361

74-
# Fetch ASGI application before importing dependencies that require ORM models.
75-
http_asgi_app = get_asgi_application()
62+
---
7663

77-
from channels.auth import AuthMiddlewareStack
78-
from channels.routing import ProtocolTypeRouter, URLRouter
64+
You'll also need to modify a few files in your Django project...
7965

80-
application = ProtocolTypeRouter(
81-
{
82-
"http": http_asgi_app,
83-
"websocket": SessionMiddlewareStack(
84-
AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
85-
),
86-
}
87-
)
88-
```
89-
90-
## `settings.py`
66+
## [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/)
9167

9268
In your settings you'll need to add `django_idom` to the
93-
[`INSTALLED_APPS`](https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-INSTALLED_APPS)
69+
[`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS)
9470
list:
9571

9672
```python
@@ -100,104 +76,63 @@ INSTALLED_APPS = [
10076
]
10177
```
10278

103-
You may configure additional options as well:
79+
You may configure additional options as well...
10480

10581
```python
106-
# the base URL for all IDOM-releated resources
107-
IDOM_BASE_URL: str = "_idom/"
82+
# If "idom" cache is not configured, then we'll use the "default" instead
83+
CACHES = {
84+
"idom": {"BACKEND": ...},
85+
}
10886

10987
# Maximum seconds between two reconnection attempts that would cause the client give up.
11088
# 0 will disable reconnection.
11189
IDOM_WS_MAX_RECONNECT_DELAY: int = 604800
11290

113-
# Configure a cache for loading JS files
114-
CACHES = {
115-
# If "idom" cache is not configured, then we'll use the "default" instead
116-
"idom": {"BACKEND": ...},
117-
}
91+
# The URL for IDOM to serve its Websockets
92+
IDOM_WEBSOCKET_URL: str = "idom/"
11893
```
11994

120-
## `urls.py`
95+
## [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/)
12196

122-
You'll need to include IDOM's static web modules path using `IDOM_WEB_MODULES_PATH`.
123-
Similarly to the `IDOM_WEBSOCKET_PATH`. If you wish to change the route where this
124-
websocket is served from, see the available [settings](#settings.py).
97+
Add Django-IDOM http URLs to your `urlpatterns`.
12598

12699
```python
127-
from django_idom import IDOM_WEB_MODULES_PATH
128-
129100
urlpatterns = [
130-
IDOM_WEB_MODULES_PATH,
101+
path("idom/", include("django_idom.http.urls")),
131102
...
132103
]
133104
```
134105

135-
## `example_app/components.py`
136-
137-
This is where, by a convention similar to that of
138-
[`views.py`](https://docs.djangoproject.com/en/3.2/topics/http/views/), you'll define
139-
your [IDOM](https://github.com/idom-team/idom) components. Ultimately though, you should
140-
feel free to organize your component modules you wish. The components created here will
141-
ultimately be referenced by name in `your-template.html`. `your-template.html`.
142-
143-
```python
144-
import idom
145-
146-
@idom.component
147-
def Hello(websocket, greeting_recipient): # component names are camelcase by convention
148-
return idom.html.header(f"Hello {greeting_recipient}!")
149-
```
150-
151-
## `example_app/templates/your-template.html`
152-
153-
In your templates, you may inject a view of an IDOM component into your templated HTML
154-
by using the `idom_component` template tag. This tag which requires the name of a component
155-
to render (of the form `module_name.ComponentName`) and keyword arguments you'd like to
156-
pass it from the template.
157-
158-
```python
159-
idom_component module_name.ComponentName param_1="something" param_2="something-else"
160-
```
161-
162-
In context this will look a bit like the following...
163-
164-
```jinja
165-
{% load idom %}
106+
## [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/)
166107

167-
<!DOCTYPE html>
168-
<html>
169-
<body>
170-
...
171-
{% idom_component "your_project.example_app.components.Hello" greeting_recipient="World" %}
172-
</body>
173-
</html>
174-
```
108+
If you do not have an `asgi.py`, first follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html) in
109+
order to create websockets within Django.
175110

176-
## `example_app/views.py`
111+
We will add IDOM's websocket consumer path using `IDOM_WEBSOCKET_PATH`.
177112

178-
You can then serve `your-template.html` from a view just
179-
[like any other](https://docs.djangoproject.com/en/3.2/intro/tutorial03/#write-views-that-actually-do-something).
113+
_Note: If you wish to change the route where this websocket is served from, see the
114+
available [settings](#settingspy)._
180115

181116
```python
182-
from django.shortcuts import render
183-
184-
def your_view(request):
185-
context = {}
186-
return render(request, "your-template.html", context)
187-
```
188117

189-
## `example_app/urls.py`
118+
import os
119+
from django.core.asgi import get_asgi_application
120+
from django_idom import IDOM_WEBSOCKET_PATH
190121

191-
Include your view in the list of urlpatterns
122+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
123+
http_asgi_app = get_asgi_application()
192124

193-
```python
194-
from django.urls import path
195-
from .views import your_view # define this view like any other HTML template view
125+
from channels.auth import AuthMiddlewareStack
126+
from channels.routing import ProtocolTypeRouter, URLRouter
196127

197-
urlpatterns = [
198-
path("", your_view),
199-
...
200-
]
128+
application = ProtocolTypeRouter(
129+
{
130+
"http": http_asgi_app,
131+
"websocket": SessionMiddlewareStack(
132+
AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
133+
),
134+
}
135+
)
201136
```
202137

203138
# Developer Guide

src/django_idom/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from .paths import IDOM_WEB_MODULES_PATH, IDOM_WEBSOCKET_PATH
1+
from .websocket.consumer import IdomWebsocket
2+
from .websocket.paths import IDOM_WEBSOCKET_PATH
23

34

45
__version__ = "0.0.1"
5-
__all__ = ["IDOM_WEB_MODULES_PATH", "IDOM_WEBSOCKET_PATH"]
6+
__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket"]

src/django_idom/config.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77

88
IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {}
99

10-
IDOM_BASE_URL = getattr(settings, "IDOM_BASE_URL", "_idom/")
11-
IDOM_WEBSOCKET_URL = IDOM_BASE_URL + "websocket/"
12-
IDOM_WEB_MODULES_URL = IDOM_BASE_URL + "web_module/"
10+
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "idom/")
1311
IDOM_WS_MAX_RECONNECT_DELAY = getattr(settings, "IDOM_WS_MAX_RECONNECT_DELAY", 604800)
1412

13+
# Determine if using Django caching or LRU cache
1514
if "idom" in getattr(settings, "CACHES", {}):
1615
IDOM_CACHE = caches["idom"]
1716
else:

src/django_idom/http/__init__.py

Whitespace-only changes.

src/django_idom/http/urls.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from django.urls import path
2+
3+
from . import views
4+
5+
6+
app_name = "idom"
7+
8+
urlpatterns = [
9+
path(
10+
"web_module/<path:file>",
11+
views.web_modules_file,
12+
name="web_modules",
13+
)
14+
]

src/django_idom/views.py renamed to src/django_idom/http/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.http import HttpRequest, HttpResponse
66
from idom.config import IDOM_WED_MODULES_DIR
77

8-
from .config import IDOM_CACHE
8+
from django_idom.config import IDOM_CACHE
99

1010

1111
async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:

src/django_idom/paths.py

-31
This file was deleted.

src/django_idom/templates/idom/component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% load static %}
22
<div id="{{ idom_mount_uuid }}" class="{{ class }}"></div>
33
<script type="module" crossorigin="anonymous">
4-
import { mountViewToElement } from "{% static '/django_idom/client.js' %}";
4+
import { mountViewToElement } from "{% static 'django_idom/client.js' %}";
55
const mountPoint = document.getElementById("{{ idom_mount_uuid }}");
66
mountViewToElement(
77
mountPoint,

src/django_idom/templatetags/idom.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
from uuid import uuid4
44

55
from django import template
6+
from django.urls import reverse
67

7-
from django_idom.config import (
8-
IDOM_WEB_MODULES_URL,
9-
IDOM_WEBSOCKET_URL,
10-
IDOM_WS_MAX_RECONNECT_DELAY,
11-
)
8+
from django_idom.config import IDOM_WEBSOCKET_URL, IDOM_WS_MAX_RECONNECT_DELAY
129
from django_idom.utils import _register_component
1310

1411

12+
IDOM_WEB_MODULES_URL = reverse("idom:web_modules", args=["x"])[:-1][1:]
1513
register = template.Library()
1614

1715

src/django_idom/websocket/__init__.py

Whitespace-only changes.

src/django_idom/websocket_consumer.py renamed to src/django_idom/websocket/consumer.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
from idom.core.dispatcher import dispatch_single_view
1313
from idom.core.layout import Layout, LayoutEvent
1414

15-
from .config import IDOM_REGISTERED_COMPONENTS
15+
from django_idom.config import IDOM_REGISTERED_COMPONENTS
1616

1717

1818
_logger = logging.getLogger(__name__)
1919

2020

2121
@dataclass
22-
class WebsocketConnection:
22+
class IdomWebsocket:
2323
scope: dict
2424
close: Callable[[Optional[int]], Awaitable[None]]
2525
disconnect: Callable[[int], Awaitable[None]]
@@ -67,7 +67,7 @@ async def _run_dispatch_loop(self):
6767
component_kwargs = json.loads(query_dict.get("kwargs", "{}"))
6868

6969
# Provide developer access to parts of this websocket
70-
socket = WebsocketConnection(self.scope, self.close, self.disconnect, view_id)
70+
socket = IdomWebsocket(self.scope, self.close, self.disconnect, view_id)
7171

7272
try:
7373
component_instance = component_constructor(socket, **component_kwargs)

0 commit comments

Comments
 (0)