Skip to content

Commit

Permalink
Add JSON context feat to SimpleStaticFileServer
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuatz committed Nov 13, 2024
1 parent 7c5d104 commit 0bdf354
Showing 1 changed file with 78 additions and 4 deletions.
82 changes: 78 additions & 4 deletions django_utils_lib/requests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import json
import os
import re
from typing import Dict, Final, List, Optional, Union
import tempfile
from typing import Dict, Final, List, Literal, Optional, TypedDict, Union

import pydantic
from django.conf import settings
Expand All @@ -13,6 +16,7 @@
)
from django.urls import re_path
from django.views.static import serve
from typing_extensions import NotRequired

from django_utils_lib.lazy import lazy_django

Expand All @@ -34,6 +38,21 @@ class SimpleStaticFileServerConfig(pydantic.BaseModel):
For example, a settings of `True` would block `/dir/index.html`, but `/dir/`
would be permissible (unless blocked by a different access rule)
"""
json_context_key: str = pydantic.Field(default="__DJANGO_CONTEXT__")
"""
If injecting JSON context into the HTML response of a request, this key will be
used to store the data, under the window global (e.g. accessible via
`window.{json_context_key}` or `globalThis.{json_context_key}`).
Default = `__DJANGO_CONTEXT__`
"""
json_context_injection_location: Literal["head", "body"] = pydantic.Field(default="head")
"""
If injecting JSON context into the HTML response of a request, this is where
(the HTML tag) in which it will be injected as a nested script tag.
Default = `"head"`
"""


class SimpleStaticFileServer:
Expand Down Expand Up @@ -82,26 +101,81 @@ def guard_path(self, request: HttpRequest, url_path: str) -> Optional[HttpRespon
# Pass request forward / noop
return None

class JSONContext(TypedDict):
data: Dict
"""
The data to inject into a request, via JSON embedded in a script tag
"""
global_key: NotRequired[Optional[str]]
"""
The global key under which to store the data / make accessible via JS
Defaults to the `config.json_context_key` value, which itself defaults to
`__DJANGO_CONTEXT__`
"""
injection_location: NotRequired[Optional[str]]
"""
Where to inject the script tag containing the JSON payload
Defaults to the `config.json_context_injection_location` value, which itself
defaults to `"head"`
"""

def serve_static_path(
self, request: HttpRequest, asset_path: str, url_path: Optional[str] = None
self,
request: HttpRequest,
asset_path: str,
url_path: Optional[str] = None,
json_data: Optional[JSONContext] = None,
) -> Union[HttpResponse, FileResponse]:
"""
This should be close to a drop-in replacement for `serve` (it wraps it),
with config-based logic for how to handle the request
Note: The difference between `asset_path` and `url_path` is that `asset_path`
> **Note**: The difference between `asset_path` and `url_path` is that `asset_path`
is the actual filesystem path (relative to staticfiles root) and `url_path`
is what the user sees as the path. They _can_ be different, but don't _have_
to be. A good use-case for having them different values is so that you can use
something like `/my_page/` as the `url_path`, but `/my_page/index.html` as the
`asset_path`.
Additionally, it supports dynamically injecting context into an HTML response,
by injecting the context as a JSON payload inside an injected script tag.
> **Warning**: This dynamic script injection has performance implications and could
probably be optimized a bit.
"""
if request.method not in ["GET", "HEAD", "OPTIONS"]:
return HttpResponseNotAllowed(["GET", "HEAD", "OPTIONS"])
url_path = url_path or request.path
if (response := self.guard_path(request, url_path)) is not None:
return response
return serve(request, document_root=str(settings.STATIC_ROOT), path=asset_path)
if json_data is None:
return serve(request, document_root=str(settings.STATIC_ROOT), path=asset_path)

if not asset_path.endswith(".html"):
raise ValueError("Cannot inject JSON context into a non-HTML asset")

# To render json data context into the page, we will inject is a script tag, with ...
global_json_key = json_data.get("global_key", self.config.json_context_key)
assert global_json_key is not None
injectable_json_script_tag_str = f"<script>window.{global_json_key} = {json.dumps(json_data['data'])};</script>"

injection_location = json_data.get("injection_location", self.config.json_context_injection_location)
assert injection_location in ["head", "body"]

raw_html_path = os.path.join(settings.STATIC_ROOT or "", asset_path.lstrip("/"))
raw_html_code = open(raw_html_path, "r").read()
if injection_location == "head":
final_html_code = raw_html_code.replace("<head>", f"<head>{injectable_json_script_tag_str}")
else:
final_html_code = raw_html_code.replace("</body>", f"{injectable_json_script_tag_str}</body>")
# safe_join
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".html") as temp_file:
temp_file.write(final_html_code)
temp_file_path = temp_file.name
print(temp_file_path)
return serve(request, document_root="/", path=temp_file_path.lstrip("/"))

def generate_url_patterns(self, ignore_start_strings: Optional[List[str]] = None):
"""
Expand Down

0 comments on commit 0bdf354

Please sign in to comment.