Skip to content

Commit f8f094e

Browse files
committed
skeleton of django installable app
1 parent 4bcd9bf commit f8f094e

22 files changed

+177
-41
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Django IDOM Build Artifacts
2+
src/django_idom/static/js
3+
14
# Django #
25
logs
36
*.log

MANIFEST.in

Whitespace-only changes.

setup.py

+63-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
import os
1+
from __future__ import print_function
2+
3+
import pipes
4+
import shutil
5+
import subprocess
26
import sys
7+
import traceback
8+
from distutils import log
9+
from distutils.command.build import build # type: ignore
10+
from distutils.command.sdist import sdist # type: ignore
311
from pathlib import Path
412

513
from setuptools import find_packages, setup
14+
from setuptools.command.develop import develop
15+
16+
17+
if sys.platform == "win32":
18+
from subprocess import list2cmdline
19+
else:
20+
21+
def list2cmdline(cmd_list):
22+
return " ".join(map(pipes.quote, cmd_list))
623

724

825
# the name of the project
@@ -31,6 +48,7 @@
3148
"license": "MIT",
3249
"platforms": "Linux, Mac OS X, Windows",
3350
"keywords": ["interactive", "widgets", "DOM", "React"],
51+
"include_package_data": True,
3452
"zip_safe": False,
3553
"classifiers": [
3654
"Framework :: Django",
@@ -52,14 +70,14 @@
5270
# Library Version
5371
# -----------------------------------------------------------------------------
5472

55-
with open(os.path.join(package_dir, "__init__.py")) as f:
56-
for line in f.read().split("\n"):
57-
if line.startswith("__version__ = "):
58-
package["version"] = eval(line.split("=", 1)[1])
59-
break
60-
else:
61-
print("No version found in %s/__init__.py" % package_dir)
62-
sys.exit(1)
73+
74+
for line in (package_dir / "__init__.py").read_text().split("\n"):
75+
if line.startswith("__version__ = "):
76+
package["version"] = eval(line.split("=", 1)[1])
77+
break
78+
else:
79+
print("No version found in %s/__init__.py" % package_dir)
80+
sys.exit(1)
6381

6482

6583
# -----------------------------------------------------------------------------
@@ -87,6 +105,42 @@
87105
package["long_description_content_type"] = "text/markdown"
88106

89107

108+
# ----------------------------------------------------------------------------
109+
# Build Javascript
110+
# ----------------------------------------------------------------------------
111+
112+
113+
def build_javascript_first(cls):
114+
class Command(cls):
115+
def run(self):
116+
log.info("Installing Javascript...")
117+
try:
118+
js_dir = str(src_dir / "js")
119+
npm = shutil.which("npm") # this is required on windows
120+
if npm is None:
121+
raise RuntimeError("NPM is not installed.")
122+
for args in (f"{npm} install", f"{npm} run build"):
123+
args_list = args.split()
124+
log.info(f"> {list2cmdline(args_list)}")
125+
subprocess.run(args_list, cwd=js_dir, check=True)
126+
except Exception:
127+
log.error("Failed to install Javascript")
128+
log.error(traceback.format_exc())
129+
raise
130+
else:
131+
log.info("Successfully installed Javascript")
132+
super().run()
133+
134+
return Command
135+
136+
137+
package["cmdclass"] = {
138+
"sdist": build_javascript_first(sdist),
139+
"build": build_javascript_first(build),
140+
"develop": build_javascript_first(develop),
141+
}
142+
143+
90144
# -----------------------------------------------------------------------------
91145
# Install It
92146
# -----------------------------------------------------------------------------

src/django_idom/app_settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.conf import settings
2+
3+
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")

src/django_idom/static/js/idom.js

-2
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
<!-- The importable IDOM JS -->
2-
<script src="{% static 'js/idom.js' %}" crossorigin="anonymous"></script>
2+
<script src="{% static 'js/idom.js' %}" crossorigin="anonymous"></script>
+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<!-- Allows for making the IDOM root object the Django way
22
Developers will often opt not to use this,
33
but having this makes getting started a little bit easier to explain -->
4-
<div id="{{ html_id }}"></div>
4+
<div id="{{ idom_view_id }}"></div>
5+
<script type="module" crossorigin="anonymous">
6+
import { mountToElement } from "{% static 'js/idom.js' %}";
7+
mountViewToElement("{{ idom_websocket_url }}", "{{ idom_view_id }}");
8+
</script>
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div id="{{ idom_view_id }}"></div>
2+
<script type="module" crossorigin="anonymous">
3+
import { mountToElement } from "{% static 'js/idom.js' %}";
4+
mountViewToElement("{{ idom_websocket_url }}", "{{ idom_view_id }}");
5+
</script>

src/django_idom/templatetags/idom.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from django import template
22

3+
from django_idom.app_settings import IDOM_WEBSOCKET_URL
4+
35

46
register = template.Library()
57

@@ -10,7 +12,6 @@ def idom_scripts():
1012
pass
1113

1214

13-
# Template tag that renders an empty idom root object
14-
@register.inclusion_tag("idom/root.html")
15-
def idom_view(html_id):
16-
return {"html_id": html_id}
15+
@register.inclusion_tag("idom/view.html")
16+
def idom_view(view_id):
17+
return {"idom_websocket_url": IDOM_WEBSOCKET_URL, "view_id": view_id}

src/django_idom/view_loader.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import logging
2+
from importlib import import_module
3+
from pathlib import Path
4+
from typing import Dict
5+
6+
from django.conf import settings
7+
from idom.core.proto import ComponentConstructor
8+
9+
10+
ALL_VIEWS: Dict[str, ComponentConstructor] = {}
11+
logger = logging.getLogger(__name__)
12+
13+
for app_name in settings.INSTALLED_APPS:
14+
app_mod = import_module(app_name)
15+
if not hasattr(app_mod, "idom"):
16+
continue
17+
18+
for idom_view_path in Path(app_mod.__file__).iterdir():
19+
if idom_view_path.suffix == ".py" and idom_view_path.is_file():
20+
idom_view_mod_name = ".".join([app_name, "idom", idom_view_path.stem])
21+
idom_view_mod = import_module(idom_view_mod_name)
22+
23+
if hasattr(idom_view_mod, "Root") and callable(idom_view_mod.Root):
24+
ALL_VIEWS[idom_view_mod_name] = idom_view_mod.Root
25+
else:
26+
logger.warning(
27+
f"Expected module {idom_view_mod_name} to expose a 'Root' "
28+
" attribute that is an IDOM component."
29+
)

src/django_idom/websocket_consumer.py

+28-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
"""Anything used to construct a websocket endpoint"""
22
import asyncio
3+
import logging
34
from typing import Any
45

56
from channels.generic.websocket import AsyncJsonWebsocketConsumer
67
from idom.core.dispatcher import dispatch_single_view
78
from idom.core.layout import Layout, LayoutEvent
8-
from idom.core.proto import ComponentConstructor
9+
10+
from .view_loader import ALL_VIEWS
11+
12+
13+
logger = logging.getLogger(__name__)
914

1015

1116
class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
1217
"""Communicates with the browser to perform actions on-demand."""
1318

14-
def __init__(
15-
self, component: ComponentConstructor, *args: Any, **kwargs: Any
16-
) -> None:
17-
self._idom_component_constructor = component
19+
def __init__(self, *args: Any, **kwargs: Any) -> None:
1820
super().__init__(*args, **kwargs)
1921

2022
async def connect(self) -> None:
@@ -32,10 +34,30 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None:
3234
await self._idom_recv_queue.put(LayoutEvent(**content))
3335

3436
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 = ...
41+
42+
if view_id not in ALL_VIEWS:
43+
logger.warning(f"Uknown IDOM view ID {view_id}")
44+
return
45+
46+
component_constructor = ALL_VIEWS[view_id]
47+
48+
try:
49+
component_instance = component_constructor(*component_args)
50+
except Exception:
51+
logger.exception(
52+
f"Failed to construct component {component_constructor} "
53+
f"with parameters {component_args}"
54+
)
55+
return
56+
3557
self._idom_recv_queue = recv_queue = asyncio.Queue()
3658
try:
3759
await dispatch_single_view(
38-
Layout(self._idom_component_constructor()),
60+
Layout(component_instance),
3961
self.send_json,
4062
recv_queue.get,
4163
)
File renamed without changes.
File renamed without changes.

tests/js/package.json renamed to src/js/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "tests",
3-
"version": "1.0.0",
2+
"name": "django-idom-client",
3+
"version": "0.0.1",
44
"description": "test app for idom_django websocket server",
55
"main": "src/index.js",
66
"files": [

tests/js/rollup.config.js renamed to src/js/rollup.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { PRODUCTION } = process.env;
77
export default {
88
input: "src/index.js",
99
output: {
10-
file: "../test_app/static/build.js",
10+
file: "../django_idom/static/js/idom.js",
1111
format: "esm",
1212
},
1313
plugins: [

src/js/src/index.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { mountLayoutWithWebSocket } from "idom-client-react";
2+
3+
4+
// Set up a websocket at the base endpoint
5+
let LOCATION = window.location;
6+
let WS_PROTOCOL = "";
7+
if (LOCATION.protocol == "https:") {
8+
WS_PROTOCOL = "wss://";
9+
} else {
10+
WS_PROTOCOL = "ws://";
11+
}
12+
let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
13+
14+
15+
export function mountViewToElement(idomWebsocketUrl, viewId) {
16+
const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl
17+
mountLayoutWithWebSocket(document.getElementById(viewId), fullWebsocketUrl);
18+
}

tests/js/src/index.js

-13
This file was deleted.

tests/test_app/asgi.py

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

1010
import os
1111

12+
from django.conf import settings
1213
from django.conf.urls import url
1314
from django.core.asgi import get_asgi_application
1415

15-
from django_idom import IdomAsyncWebSocketConsumer # noqa: E402
16+
from django_idom import IdomAsyncWebSocketConsumer
17+
from django_idom.app_settings import IDOM_WEBSOCKET_URL # noqa: E402
1618

1719
from .views import Root
1820

@@ -25,11 +27,14 @@
2527
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
2628

2729

30+
IDOM_WEBSOCKET_URL = settings.IDOM_WEBSOCKET_URL
31+
32+
2833
application = ProtocolTypeRouter(
2934
{
3035
"http": http_asgi_app,
3136
"websocket": URLRouter(
32-
[url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))]
37+
[url(IDOM_WEBSOCKET_URL, IdomAsyncWebSocketConsumer.as_asgi())]
3338
),
3439
}
3540
)

tests/test_app/idom/__init__.py

Whitespace-only changes.

tests/test_app/idom/button.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import idom
2+
3+
4+
@idom.component
5+
def Root():
6+
...

tests/test_app/idom/hello_world.py

Whitespace-only changes.

tests/test_app/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"django.contrib.messages",
3838
"django.contrib.staticfiles",
3939
"channels", # Websocket library
40+
"django_idom", # Django compatible IDOM client
4041
"test_app", # This test application
4142
]
4243
MIDDLEWARE = [

0 commit comments

Comments
 (0)