Skip to content

Commit 937da20

Browse files
committed
get test_app runserver to work
1 parent f8f094e commit 937da20

23 files changed

+144
-127
lines changed

MANIFEST.in

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include src/django_idom/static/js/idom.js
2+
include src/django_idom/templates/head_content.html
3+
include src/django_idom/templates/view.html

noxfile.py

-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def test_suite(session: Session) -> None:
5252
session.chdir(HERE / "tests")
5353
session.env["IDOM_DEBUG_MODE"] = "1"
5454
session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs))
55-
session.run("python", "manage.py", "build_js")
5655
session.run("python", "manage.py", "test")
5756

5857

src/django_idom/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
from .websocket_consumer import IdomAsyncWebSocketConsumer
1+
from .websocket_consumer import (
2+
IdomAsyncWebSocketConsumer,
3+
django_idom_websocket_consumer_url,
4+
)
25

36

47
__version__ = "0.0.1"
5-
__all__ = ["IdomAsyncWebSocketConsumer"]
8+
__all__ = ["IdomAsyncWebSocketConsumer", "django_idom_websocket_consumer_url"]

src/django_idom/app_components.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import logging
2+
from importlib import import_module
3+
from typing import Dict
4+
5+
from django.conf import settings
6+
from idom.core.proto import ComponentConstructor
7+
8+
9+
logger = logging.getLogger(__name__)
10+
_LOADED_COMPONENTS: Dict[str, ComponentConstructor] = {}
11+
12+
13+
def get_component(name: str) -> ComponentConstructor:
14+
return _LOADED_COMPONENTS[name]
15+
16+
17+
def has_component(name: str) -> bool:
18+
return name in _LOADED_COMPONENTS
19+
20+
21+
for app_mod_name in settings.INSTALLED_APPS:
22+
idom_mod_name = f"{app_mod_name}.idom"
23+
24+
try:
25+
idom_mod = import_module(idom_mod_name)
26+
except ImportError:
27+
logger.debug(f"Skipping {idom_mod_name!r} - does not exist")
28+
continue
29+
30+
if not hasattr(idom_mod, "__all__"):
31+
logger.warning(
32+
f"'django_idom' expected module {idom_mod_name!r} to have an "
33+
"'__all__' attribute that lists its publically available components."
34+
)
35+
continue
36+
37+
for component_name in idom_mod.__all__:
38+
try:
39+
component_constructor = getattr(idom_mod, component_name)
40+
except AttributeError:
41+
logger.warning(
42+
f"Module {idom_mod_name!r} has no attribute {component_name!r}"
43+
)
44+
continue
45+
46+
if not callable(component_constructor):
47+
logger.warning(f"'{idom_mod_name}.{component_name}' is not a component")
48+
continue
49+
50+
_LOADED_COMPONENTS[f"{app_mod_name}.{component_name}"] = component_constructor

src/django_idom/app_settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
from django.conf import settings
22

3+
34
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")
5+
if not IDOM_WEBSOCKET_URL.endswith("/"):
6+
IDOM_WEBSOCKET_URL += "/"

src/django_idom/templates/idom/root.html

-8
This file was deleted.
+10-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
<div id="{{ idom_view_id }}"></div>
1+
{% load static %}
2+
<div id="{{ idom_mount_uuid }}"></div>
23
<script type="module" crossorigin="anonymous">
3-
import { mountToElement } from "{% static 'js/idom.js' %}";
4-
mountViewToElement("{{ idom_websocket_url }}", "{{ idom_view_id }}");
4+
import { mountViewToElement } from "{% static 'js/idom.js' %}";
5+
const mountPoint = document.getElementById("{{ idom_mount_uuid }}");
6+
mountViewToElement(
7+
mountPoint,
8+
"{{ idom_websocket_url }}",
9+
"{{ idom_view_id }}",
10+
"{{ idom_view_params }}"
11+
);
512
</script>

src/django_idom/templatetags/idom.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from uuid import uuid4
2+
13
from django import template
24

35
from django_idom.app_settings import IDOM_WEBSOCKET_URL
@@ -8,10 +10,15 @@
810

911
# Template tag that renders the IDOM scripts
1012
@register.inclusion_tag("idom/head_content.html")
11-
def idom_scripts():
13+
def idom_head():
1214
pass
1315

1416

1517
@register.inclusion_tag("idom/view.html")
16-
def idom_view(view_id):
17-
return {"idom_websocket_url": IDOM_WEBSOCKET_URL, "view_id": view_id}
18+
def idom_view(view_id, view_params=""):
19+
return {
20+
"idom_websocket_url": IDOM_WEBSOCKET_URL,
21+
"idom_mount_uuid": uuid4().hex,
22+
"idom_view_id": view_id,
23+
"idom_view_params": view_params,
24+
}

src/django_idom/view_loader.py

-29
This file was deleted.

src/django_idom/websocket_consumer.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,35 @@
22
import asyncio
33
import logging
44
from typing import Any
5+
from urllib.parse import parse_qsl
56

67
from channels.generic.websocket import AsyncJsonWebsocketConsumer
8+
from django.urls import path
79
from idom.core.dispatcher import dispatch_single_view
810
from idom.core.layout import Layout, LayoutEvent
911

10-
from .view_loader import ALL_VIEWS
12+
from .app_components import get_component, has_component
13+
from .app_settings import IDOM_WEBSOCKET_URL
1114

1215

1316
logger = logging.getLogger(__name__)
1417

1518

19+
def django_idom_websocket_consumer_url(*args, **kwargs):
20+
"""Return a URL resolver for :class:`IdomAsyncWebSocketConsumer`
21+
22+
While this is relatively uncommon in most Django apps, because the URL of the
23+
websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
24+
to allow users to configure the URL themselves
25+
"""
26+
return path(
27+
IDOM_WEBSOCKET_URL + "<view_id>/",
28+
IdomAsyncWebSocketConsumer.as_asgi(),
29+
*args,
30+
**kwargs,
31+
)
32+
33+
1634
class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
1735
"""Communicates with the browser to perform actions on-demand."""
1836

@@ -34,23 +52,21 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None:
3452
await self._idom_recv_queue.put(LayoutEvent(**content))
3553

3654
async def _run_dispatch_loop(self):
37-
# get the URL parameters and grab the view ID
38-
view_id = ...
39-
# get component ags from the URL params too
40-
component_args = ...
55+
view_id = self.scope["url_route"]["kwargs"]["view_id"]
4156

42-
if view_id not in ALL_VIEWS:
43-
logger.warning(f"Uknown IDOM view ID {view_id}")
57+
if not has_component(view_id):
58+
logger.warning(f"Uknown IDOM view ID {view_id!r}")
4459
return
4560

46-
component_constructor = ALL_VIEWS[view_id]
61+
component_constructor = get_component(view_id)
62+
component_kwargs = dict(parse_qsl(self.scope["query_string"]))
4763

4864
try:
49-
component_instance = component_constructor(*component_args)
65+
component_instance = component_constructor(**component_kwargs)
5066
except Exception:
5167
logger.exception(
5268
f"Failed to construct component {component_constructor} "
53-
f"with parameters {component_args}"
69+
f"with parameters {component_kwargs}"
5470
)
5571
return
5672

src/js/package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/js/src/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ if (LOCATION.protocol == "https:") {
1212
let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
1313

1414

15-
export function mountViewToElement(idomWebsocketUrl, viewId) {
16-
const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl
17-
mountLayoutWithWebSocket(document.getElementById(viewId), fullWebsocketUrl);
15+
export function mountViewToElement(mountPoint, idomWebsocketUrl, viewId, queryParams) {
16+
const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/";
17+
mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl);
1818
}

tests/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
test_app/static/build.js
1+
*.sqlite3

tests/test_app/asgi.py

+2-12
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@
99

1010
import os
1111

12-
from django.conf import settings
13-
from django.conf.urls import url
1412
from django.core.asgi import get_asgi_application
1513

16-
from django_idom import IdomAsyncWebSocketConsumer
17-
from django_idom.app_settings import IDOM_WEBSOCKET_URL # noqa: E402
18-
19-
from .views import Root
14+
from django_idom import django_idom_websocket_consumer_url
2015

2116

2217
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
@@ -27,14 +22,9 @@
2722
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
2823

2924

30-
IDOM_WEBSOCKET_URL = settings.IDOM_WEBSOCKET_URL
31-
32-
3325
application = ProtocolTypeRouter(
3426
{
3527
"http": http_asgi_app,
36-
"websocket": URLRouter(
37-
[url(IDOM_WEBSOCKET_URL, IdomAsyncWebSocketConsumer.as_asgi())]
38-
),
28+
"websocket": URLRouter([django_idom_websocket_consumer_url()]),
3929
}
4030
)

tests/test_app/idom.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import idom
2+
3+
4+
__all__ = "HelloWorld", "Button"
5+
6+
7+
@idom.component
8+
def HelloWorld():
9+
return idom.html.h1({"id": "hello-world"}, "Hello World!")
10+
11+
12+
@idom.component
13+
def Button():
14+
count, set_count = idom.hooks.use_state(0)
15+
return idom.html.div(
16+
idom.html.button(
17+
{"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
18+
"Click me!",
19+
),
20+
idom.html.p(
21+
{"id": "counter-num", "data-count": count},
22+
f"Current count is: {count}",
23+
),
24+
)

tests/test_app/idom/__init__.py

Whitespace-only changes.

tests/test_app/idom/button.py

-6
This file was deleted.

tests/test_app/idom/hello_world.py

Whitespace-only changes.

tests/test_app/management/__init__.py

Whitespace-only changes.

tests/test_app/management/commands/__init__.py

Whitespace-only changes.

tests/test_app/management/commands/build_js.py

-16
This file was deleted.

tests/test_app/templates/base.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% load static %}
1+
{% load static %} {% load idom %}
22
<!DOCTYPE html>
33
<html lang="en">
44
<head>
@@ -15,7 +15,7 @@
1515

1616
<body>
1717
<h1>IDOM Test Page</h1>
18-
<div id="mount"></div>
19-
<script src="{% static 'build.js' %}" crossorigin="anonymous"></script>
18+
<div>{% idom_view "test_app.HelloWorld" %}</div>
19+
<div>{% idom_view "test_app.Button" %}</div>
2020
</body>
2121
</html>

tests/test_app/views.py

-26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import idom
21
from django.http import HttpResponse
32
from django.template import loader
43

@@ -7,28 +6,3 @@ def base_template(request):
76
template = loader.get_template("base.html")
87
context = {}
98
return HttpResponse(template.render(context, request))
10-
11-
12-
@idom.component
13-
def Root():
14-
return idom.html.div(HelloWorld(), Counter())
15-
16-
17-
@idom.component
18-
def HelloWorld():
19-
return idom.html.h1({"id": "hello-world"}, "Hello World!")
20-
21-
22-
@idom.component
23-
def Counter():
24-
count, set_count = idom.hooks.use_state(0)
25-
return idom.html.div(
26-
idom.html.button(
27-
{"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
28-
"Click me!",
29-
),
30-
idom.html.p(
31-
{"id": "counter-num", "data-count": count},
32-
f"Current count is: {count}",
33-
),
34-
)

0 commit comments

Comments
 (0)