diff --git a/src/ims/application/_main.py b/src/ims/application/_main.py
index 521e6da1f..4725ac866 100644
--- a/src/ims/application/_main.py
+++ b/src/ims/application/_main.py
@@ -29,7 +29,8 @@
import ims.element
from ims.config import Configuration, URLs
from ims.dms import DutyManagementSystem
-from ims.ext.klein import KleinRenderable
+from ims.ext.json import jsonTextFromObject
+from ims.ext.klein import ContentType, HeaderName, KleinRenderable, static
from ._api import APIApplication
from ._auth import AuthApplication
@@ -118,7 +119,8 @@ def __del__(self) -> None:
#
@router.route(URLs.root, methods=("HEAD", "GET"))
- def rootResource(self, request: IRequest) -> KleinRenderable:
+ @static
+ def rootEndpoint(self, request: IRequest) -> KleinRenderable:
"""
Server root page.
@@ -128,15 +130,42 @@ def rootResource(self, request: IRequest) -> KleinRenderable:
@router.route(URLs.static, branch=True)
- def static(self, request: IRequest) -> KleinRenderable:
+ @static
+ def staticEndpoint(self, request: IRequest) -> KleinRenderable:
return File(resourcesDirectory.path)
+ #
+ # URLs
+ #
+
+ @router.route(URLs.urlsJS, methods=("HEAD", "GET"))
+ @static
+ def urlsEndpoint(self, request: IRequest) -> KleinRenderable:
+ """
+ JavaScript variables for service URLs.
+ """
+ urls = {
+ k: getattr(URLs, k).asText() for k in URLs.__dict__
+ if not k.startswith("_")
+ }
+
+ request.setHeader(
+ HeaderName.contentType.value, ContentType.javascript.value
+ )
+
+ return "\n".join((
+ "var url_{} = {};".format(k, jsonTextFromObject(v))
+ for k, v in urls.items()
+ ))
+
+
#
# Child application endpoints
#
@router.route(URLs.api, branch=True)
+ @static
def apiApplicationEndpoint(self, request: IRequest) -> KleinRenderable:
"""
API application resource.
@@ -145,6 +174,7 @@ def apiApplicationEndpoint(self, request: IRequest) -> KleinRenderable:
@router.route(URLs.auth, branch=True)
+ @static
def authApplicationEndpoint(self, request: IRequest) -> KleinRenderable:
"""
Auth application resource.
@@ -153,6 +183,7 @@ def authApplicationEndpoint(self, request: IRequest) -> KleinRenderable:
@router.route(URLs.external, branch=True)
+ @static
def externalApplicationEndpoint(
self, request: IRequest
) -> KleinRenderable:
@@ -163,6 +194,7 @@ def externalApplicationEndpoint(
@router.route(URLs.app, branch=True)
+ @static
def webApplicationEndpoint(self, request: IRequest) -> KleinRenderable:
"""
Web application resource.
diff --git a/src/ims/config/_urls.py b/src/ims/config/_urls.py
index 30470a20d..7b78c3d8e 100644
--- a/src/ims/config/_urls.py
+++ b/src/ims/config/_urls.py
@@ -38,6 +38,7 @@ class URLs(object):
root = URL.fromText("/")
prefix = root.child("ims").child("")
+ urlsJS = prefix.child("urls.js")
# Static resources
static = prefix.child("static")
diff --git a/src/ims/element/_page.py b/src/ims/element/_page.py
index 7a9cf61a1..ea746093c 100644
--- a/src/ims/element/_page.py
+++ b/src/ims/element/_page.py
@@ -18,6 +18,11 @@
Element base classes.
"""
+from collections import OrderedDict
+from typing import Iterable, MutableMapping
+
+from hyperlink import URL
+
from twisted.web.iweb import IRequest
from twisted.web.template import Tag, renderer, tags
@@ -44,6 +49,46 @@ def __init__(self, config: Configuration, title: str) -> None:
self.titleText = title
+ def urlsFromImportSpec(self, spec: str) -> Iterable[URL]:
+ """
+ Given a string specifying desired imports, return the corresponding
+ URLs.
+ """
+ urls = self.config.urls
+
+ result: MutableMapping[str, URL] = OrderedDict()
+
+ def add(name: str) -> None:
+ if not name or name in result:
+ return
+
+ if name == "bootstrap":
+ add("jquery")
+
+ if name == "ims":
+ add("jquery")
+ add("moment")
+
+ try:
+ result[name] = getattr(urls, "{}JS".format(name))
+ except AttributeError:
+ raise ValueError(
+ "Invalid import {!r} in spec {!r}".format(name, spec)
+ )
+
+ if name == "dataTables":
+ add("dataTablesBootstrap")
+
+ # All pages use Bootstrap
+ add("bootstrap")
+ add("urls")
+
+ for name in spec.split(","):
+ add(name.strip())
+
+ return result.values()
+
+
@renderer
def title(
self, request: IRequest, tag: Tag = tags.title
@@ -62,14 +107,24 @@ def title(
@renderer
def head(self, request: IRequest, tag: Tag = tags.head) -> KleinRenderable:
"""
-
element.
+ `` element.
"""
urls = self.config.urls
children = tag.children
tag.children = []
+ imports = (
+ tags.script(src=url.asText())
+ for url in
+ self.urlsFromImportSpec(tag.attributes.get("imports", ""))
+ )
+
+ if "imports" in tag.attributes:
+ del tag.attributes["imports"]
+
return tag(
+ # Resource metadata
tags.meta(charset="utf-8"),
tags.meta(
name="viewport", content="width=device-width, initial-scale=1"
@@ -86,9 +141,10 @@ def head(self, request: IRequest, tag: Tag = tags.head) -> KleinRenderable:
type="text/css", rel="stylesheet", media="screen",
href=urls.styleSheet.asText(),
),
- tags.script(src=urls.jqueryJS.asText()),
- tags.script(src=urls.bootstrapJS.asText()),
self.title(request),
+ # JavaScript resource imports
+ imports,
+ # Child elements
children,
)
@@ -127,7 +183,7 @@ def bottom(
@renderer
def nav(self, request: IRequest, tag: Tag = tags.nav) -> KleinRenderable:
"""
-