Repository: reactive-python/reactpy-django (language: python)

Issue:
Fix antipattern with IDOM_WEB_MODULE_PATH
### Old Behavior

Currently, we expect the user to add `IDOM_WEB_MODULE_PATH` within the urlpattern, however, this is an antipattern within the Django framework

### New Behavior

Follow Django conventions and utilize standard path variables.

### Implementation Details

Replace instructions to utilize `IDOM_WEB_MODULE_PATH` with `path("idom/", include("django_idom.http.urls"))`

Create `django_idom.http.urls` that contains a `urlpatterns = [...]`

Anywhere within `django-idom` where we need to determine the URL, we need to fetch it using django `reverse("idom:web_modules")`

This implementation also requires that we remove `IDOM_BASE_URL` as a variable.

### Code of Conduct

- [X] I agree to follow the [Code of Conduct](https://github.com/idom-team/django-idom/blob/main/CODE_OF_CONDUCT.md).


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 788708ee..77dd0e31 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,6 @@
-# Django IDOM
-
-<p>
-  <a href="https://github.com/idom-team/django-idom/actions?query=workflow%3ATest">
-    <img alt="Tests" src="https://github.com/idom-team/django-idom/workflows/Test/badge.svg?event=push" />
-  </a>
-  <a href="https://pypi.python.org/pypi/django-idom">
-    <img alt="Version Info" src="https://img.shields.io/pypi/v/django-idom.svg"/>
-  </a>
-  <a href="https://github.com/idom-team/django-idom/blob/main/LICENSE">
-    <img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-purple.svg">
-  </a>
-</p>
-
-`django-idom` allows Django to integrate with [IDOM](https://github.com/idom-team/idom),
-a package inspired by [ReactJS](https://reactjs.org/) for creating responsive web
-interfaces in pure Python.
+# 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)
+
+`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**.
 
 **You can try IDOM now in a Jupyter Notebook:**
 <a
@@ -27,70 +13,60 @@ interfaces in pure Python.
     src="https://mybinder.org/badge_logo.svg"/>
 </a>
 
-# Install Django IDOM
+# Quick Example
 
-```bash
-pip install django-idom
-```
+## `example_app/components.py`
+
+This is where you'll define your [IDOM](https://github.com/idom-team/idom) components. Ultimately though, you should
+feel free to organize your component modules you wish. Any components created will ultimately be referenced
+by Python dotted path in `your-template.html`.
 
-# Django Integration
+```python
+from idom import component, html
+from django_idom import IdomWebsocket
 
-To integrate IDOM into your application you'll need to modify or add the following files to `your_project`:
 
-```
-your_project/
-├── __init__.py
-├── asgi.py
-├── settings.py
-├── urls.py
-└── example_app/
-    ├── __init__.py
-    ├── components.py
-    ├── templates/
-    │   └── your-template.html
-    └── urls.py
+@component
+def Hello(websocket: IdomWebsocket, greeting_recipient: str):  # Names are CamelCase by ReactJS convention
+    return html.header(f"Hello {greeting_recipient}!")
 ```
 
-## `asgi.py`
+## [`example_app/templates/your-template.html`](https://docs.djangoproject.com/en/dev/topics/templates/)
 
-Follow the [`channels`](https://channels.readthedocs.io/en/stable/)
-[installation guide](https://channels.readthedocs.io/en/stable/installation.html) in
-order to create ASGI websockets within Django. Then, we will add a path for IDOM's
-websocket consumer using `IDOM_WEBSOCKET_PATH`.
+In your templates, you may add IDOM components into your HTML by using the `idom_component`
+template tag. This tag requires the dotted path to the component function. Additonally, you can
+pass in keyworded arguments into your component function.
 
-_Note: If you wish to change the route where this websocket is served from, see the
-available [settings](#settingspy)._
+In context this will look a bit like the following...
 
-```python
+```jinja
+{% load idom %}
 
-import os
+<!DOCTYPE html>
+<html>
+  <body>
+    ...
+    {% idom_component "my_django_project.example_app.components.Hello" greeting_recipient="World" %}
+  </body>
+</html>
+```
 
-from django.core.asgi import get_asgi_application
+# Installation
 
-from django_idom import IDOM_WEBSOCKET_PATH
+Install `django-idom` via pip.
 
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
+```bash
+pip install django-idom
+```
 
-# Fetch ASGI application before importing dependencies that require ORM models.
-http_asgi_app = get_asgi_application()
+---
 
-from channels.auth import AuthMiddlewareStack
-from channels.routing import ProtocolTypeRouter, URLRouter
+You'll also need to modify a few files in your Django project...
 
-application = ProtocolTypeRouter(
-    {
-        "http": http_asgi_app,
-        "websocket": SessionMiddlewareStack(
-            AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
-        ),
-    }
-)
-```
-
-## `settings.py`
+## [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/)
 
 In your settings you'll need to add `django_idom` to the
-[`INSTALLED_APPS`](https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-INSTALLED_APPS)
+[`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS)
 list:
 
 ```python
@@ -100,104 +76,63 @@ INSTALLED_APPS = [
 ]
 ```
 
-You may configure additional options as well:
+You may configure additional options as well...
 
 ```python
-# the base URL for all IDOM-releated resources
-IDOM_BASE_URL: str = "_idom/"
+# If "idom" cache is not configured, then we'll use the "default" instead
+CACHES = {
+  "idom": {"BACKEND": ...},
+}
 
 # Maximum seconds between two reconnection attempts that would cause the client give up.
 # 0 will disable reconnection.
 IDOM_WS_MAX_RECONNECT_DELAY: int = 604800
 
-# Configure a cache for loading JS files
-CACHES = {
-  # If "idom" cache is not configured, then we'll use the "default" instead
-  "idom": {"BACKEND": ...},
-}
+# The URL for IDOM to serve its Websockets
+IDOM_WEBSOCKET_URL: str = "idom/"
 ```
 
-## `urls.py`
+## [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/)
 
-You'll need to include IDOM's static web modules path using `IDOM_WEB_MODULES_PATH`.
-Similarly to the `IDOM_WEBSOCKET_PATH`. If you wish to change the route where this
-websocket is served from, see the available [settings](#settings.py).
+Add Django-IDOM http URLs to your `urlpatterns`.
 
 ```python
-from django_idom import IDOM_WEB_MODULES_PATH
-
 urlpatterns = [
-    IDOM_WEB_MODULES_PATH,
+    path("idom/", include("django_idom.http.urls")),
     ...
 ]
 ```
 
-## `example_app/components.py`
-
-This is where, by a convention similar to that of
-[`views.py`](https://docs.djangoproject.com/en/3.2/topics/http/views/), you'll define
-your [IDOM](https://github.com/idom-team/idom) components. Ultimately though, you should
-feel free to organize your component modules you wish. The components created here will
-ultimately be referenced by name in `your-template.html`. `your-template.html`.
-
-```python
-import idom
-
-@idom.component
-def Hello(websocket, greeting_recipient):  # component names are camelcase by convention
-    return idom.html.header(f"Hello {greeting_recipient}!")
-```
-
-## `example_app/templates/your-template.html`
-
-In your templates, you may inject a view of an IDOM component into your templated HTML
-by using the `idom_component` template tag. This tag which requires the name of a component
-to render (of the form `module_name.ComponentName`) and keyword arguments you'd like to
-pass it from the template.
-
-```python
-idom_component module_name.ComponentName param_1="something" param_2="something-else"
-```
-
-In context this will look a bit like the following...
-
-```jinja
-{% load idom %}
+## [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/)
 
-<!DOCTYPE html>
-<html>
-  <body>
-    ...
-    {% idom_component "your_project.example_app.components.Hello" greeting_recipient="World" %}
-  </body>
-</html>
-```
+If you do not have an `asgi.py`, first follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html) in
+order to create websockets within Django.
 
-## `example_app/views.py`
+We will add IDOM's websocket consumer path using `IDOM_WEBSOCKET_PATH`.
 
-You can then serve `your-template.html` from a view just
-[like any other](https://docs.djangoproject.com/en/3.2/intro/tutorial03/#write-views-that-actually-do-something).
+_Note: If you wish to change the route where this websocket is served from, see the
+available [settings](#settingspy)._
 
 ```python
-from django.shortcuts import render
-
-def your_view(request):
-    context = {}
-    return render(request, "your-template.html", context)
-```
 
-## `example_app/urls.py`
+import os
+from django.core.asgi import get_asgi_application
+from django_idom import IDOM_WEBSOCKET_PATH
 
-Include your view in the list of urlpatterns
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
+http_asgi_app = get_asgi_application()
 
-```python
-from django.urls import path
-from .views import your_view  # define this view like any other HTML template view
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
 
-urlpatterns = [
-    path("", your_view),
-    ...
-]
+application = ProtocolTypeRouter(
+    {
+        "http": http_asgi_app,
+        "websocket": SessionMiddlewareStack(
+            AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
+        ),
+    }
+)
 ```
 
 # Developer Guide
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index 90a4aba1..f94066ea 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -1,5 +1,6 @@
-from .paths import IDOM_WEB_MODULES_PATH, IDOM_WEBSOCKET_PATH
+from .websocket.consumer import IdomWebsocket
+from .websocket.paths import IDOM_WEBSOCKET_PATH
 
 
 __version__ = "0.0.1"
-__all__ = ["IDOM_WEB_MODULES_PATH", "IDOM_WEBSOCKET_PATH"]
+__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket"]
diff --git a/src/django_idom/config.py b/src/django_idom/config.py
index 0664c964..37d78ad0 100644
--- a/src/django_idom/config.py
+++ b/src/django_idom/config.py
@@ -7,11 +7,10 @@
 
 IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {}
 
-IDOM_BASE_URL = getattr(settings, "IDOM_BASE_URL", "_idom/")
-IDOM_WEBSOCKET_URL = IDOM_BASE_URL + "websocket/"
-IDOM_WEB_MODULES_URL = IDOM_BASE_URL + "web_module/"
+IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "idom/")
 IDOM_WS_MAX_RECONNECT_DELAY = getattr(settings, "IDOM_WS_MAX_RECONNECT_DELAY", 604800)
 
+# Determine if using Django caching or LRU cache
 if "idom" in getattr(settings, "CACHES", {}):
     IDOM_CACHE = caches["idom"]
 else:
diff --git a/src/django_idom/http/__init__.py b/src/django_idom/http/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/django_idom/http/urls.py b/src/django_idom/http/urls.py
new file mode 100644
index 00000000..019a603e
--- /dev/null
+++ b/src/django_idom/http/urls.py
@@ -0,0 +1,14 @@
+from django.urls import path
+
+from . import views
+
+
+app_name = "idom"
+
+urlpatterns = [
+    path(
+        "web_module/<path:file>",
+        views.web_modules_file,
+        name="web_modules",
+    )
+]
diff --git a/src/django_idom/views.py b/src/django_idom/http/views.py
similarity index 97%
rename from src/django_idom/views.py
rename to src/django_idom/http/views.py
index 4f0e4c82..552d7418 100644
--- a/src/django_idom/views.py
+++ b/src/django_idom/http/views.py
@@ -5,7 +5,7 @@
 from django.http import HttpRequest, HttpResponse
 from idom.config import IDOM_WED_MODULES_DIR
 
-from .config import IDOM_CACHE
+from django_idom.config import IDOM_CACHE
 
 
 async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
diff --git a/src/django_idom/paths.py b/src/django_idom/paths.py
deleted file mode 100644
index 184182c3..00000000
--- a/src/django_idom/paths.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.urls import path
-
-from . import views
-from .config import IDOM_WEB_MODULES_URL, IDOM_WEBSOCKET_URL
-from .websocket_consumer import IdomAsyncWebsocketConsumer
-
-
-IDOM_WEBSOCKET_PATH = path(
-    IDOM_WEBSOCKET_URL + "<view_id>/",
-    IdomAsyncWebsocketConsumer.as_asgi(),
-    name="idom_websocket",
-)
-"""A URL resolver for :class:`IdomAsyncWebsocketConsumer`
-
-While this is relatively uncommon in most Django apps, because the URL of the
-websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
-to allow users to configure the URL themselves.
-"""
-
-
-IDOM_WEB_MODULES_PATH = path(
-    IDOM_WEB_MODULES_URL + "<path:file>",
-    views.web_modules_file,
-    name="idom_web_modules",
-)
-"""A URL resolver for static web modules required by IDOM
-
-While this is relatively uncommon in most Django apps, because the URL of the
-websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
-to allow users to configure the URL themselves.
-"""
diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html
index e471c6a9..8509bceb 100644
--- a/src/django_idom/templates/idom/component.html
+++ b/src/django_idom/templates/idom/component.html
@@ -1,7 +1,7 @@
 {% load static %}
 <div id="{{ idom_mount_uuid }}" class="{{ class }}"></div>
 <script type="module" crossorigin="anonymous">
-  import { mountViewToElement } from "{% static '/django_idom/client.js' %}";
+  import { mountViewToElement } from "{% static 'django_idom/client.js' %}";
   const mountPoint = document.getElementById("{{ idom_mount_uuid }}");
   mountViewToElement(
     mountPoint,
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index d6692b5b..d21620bc 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -3,15 +3,13 @@
 from uuid import uuid4
 
 from django import template
+from django.urls import reverse
 
-from django_idom.config import (
-    IDOM_WEB_MODULES_URL,
-    IDOM_WEBSOCKET_URL,
-    IDOM_WS_MAX_RECONNECT_DELAY,
-)
+from django_idom.config import IDOM_WEBSOCKET_URL, IDOM_WS_MAX_RECONNECT_DELAY
 from django_idom.utils import _register_component
 
 
+IDOM_WEB_MODULES_URL = reverse("idom:web_modules", args=["x"])[:-1][1:]
 register = template.Library()
 
 
diff --git a/src/django_idom/websocket/__init__.py b/src/django_idom/websocket/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket/consumer.py
similarity index 94%
rename from src/django_idom/websocket_consumer.py
rename to src/django_idom/websocket/consumer.py
index 349a8366..8f773a38 100644
--- a/src/django_idom/websocket_consumer.py
+++ b/src/django_idom/websocket/consumer.py
@@ -12,14 +12,14 @@
 from idom.core.dispatcher import dispatch_single_view
 from idom.core.layout import Layout, LayoutEvent
 
-from .config import IDOM_REGISTERED_COMPONENTS
+from django_idom.config import IDOM_REGISTERED_COMPONENTS
 
 
 _logger = logging.getLogger(__name__)
 
 
 @dataclass
-class WebsocketConnection:
+class IdomWebsocket:
     scope: dict
     close: Callable[[Optional[int]], Awaitable[None]]
     disconnect: Callable[[int], Awaitable[None]]
@@ -67,7 +67,7 @@ async def _run_dispatch_loop(self):
         component_kwargs = json.loads(query_dict.get("kwargs", "{}"))
 
         # Provide developer access to parts of this websocket
-        socket = WebsocketConnection(self.scope, self.close, self.disconnect, view_id)
+        socket = IdomWebsocket(self.scope, self.close, self.disconnect, view_id)
 
         try:
             component_instance = component_constructor(socket, **component_kwargs)
diff --git a/src/django_idom/websocket/paths.py b/src/django_idom/websocket/paths.py
new file mode 100644
index 00000000..0d2920e0
--- /dev/null
+++ b/src/django_idom/websocket/paths.py
@@ -0,0 +1,14 @@
+from django.urls import path
+
+from django_idom.config import IDOM_WEBSOCKET_URL
+
+from .consumer import IdomAsyncWebsocketConsumer
+
+
+IDOM_WEBSOCKET_PATH = path(
+    IDOM_WEBSOCKET_URL + "<view_id>/", IdomAsyncWebsocketConsumer.as_asgi()
+)
+"""A URL path for :class:`IdomAsyncWebsocketConsumer`.
+
+Required in order for IDOM to know the websocket path.
+"""


Test patch:
diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py
index 1ea0c99b..525659ea 100644
--- a/tests/test_app/urls.py
+++ b/tests/test_app/urls.py
@@ -18,15 +18,13 @@
     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 """
 from django.contrib import admin
-from django.urls import path
-
-from django_idom import IDOM_WEB_MODULES_PATH
+from django.urls import include, path
 
 from .views import base_template
 
 
 urlpatterns = [
     path("", base_template),
-    IDOM_WEB_MODULES_PATH,
+    path("idom/", include("django_idom.http.urls")),
     path("admin/", admin.site.urls),
 ]