Skip to content

Commit

Permalink
Merge branch 'sample'
Browse files Browse the repository at this point in the history
  • Loading branch information
danialkeimasi committed Aug 12, 2023
2 parents bc90aac + 4dd7dcf commit e15d0de
Show file tree
Hide file tree
Showing 22 changed files with 994 additions and 39 deletions.
89 changes: 61 additions & 28 deletions django_nextjs/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from multidict import MultiMapping

from .app_settings import NEXTJS_SERVER_URL
from .utils import filter_mapping_obj


def _get_render_context(html: str, extra_context: Union[Dict, None] = None):
Expand Down Expand Up @@ -51,36 +52,67 @@ def _get_nextjs_request_cookies(request: HttpRequest):
def _get_nextjs_request_headers(request: HttpRequest, headers: Union[Dict, None] = None):
# These headers are used by NextJS to indicate if a request is expecting a full HTML
# response, or an RSC response.
server_component_header_names = [
"Rsc",
"Next-Router-State-Tree",
"Next-Router-Prefetch",
"Next-Url",
"Cookie",
"Accept-Encoding",
]

server_component_headers = {}

for server_component_header in server_component_header_names:
if request.headers.get(server_component_header) is not None:
server_component_headers[server_component_header.lower()] = request.headers[server_component_header]
server_component_headers = filter_mapping_obj(
request.headers,
selected_keys=[
"Rsc",
"Next-Router-State-Tree",
"Next-Router-Prefetch",
"Next-Url",
"Cookie",
"Accept-Encoding",
],
)

return {
"x-real-ip": request.headers.get("X-Real-Ip", "") or request.META.get("REMOTE_ADDR", ""),
"user-agent": request.headers.get("User-Agent", ""),
**server_component_headers,
**({} if headers is None else headers),
**(headers or {}),
}


def _get_nextjs_response_headers(headers: MultiMapping[str]) -> Dict:
return filter_mapping_obj(
headers,
selected_keys=[
"Location",
"Vary",
"Content-Type",
],
)


async def _render_extended_template(
template_name: str,
extra_context: Union[Dict, None] = None,
request: Union[HttpRequest, None] = None,
using: Union[str, None] = None,
):
"""
Split customization template to usable parts.
"""
SEPARATOR = "____DJANGO_NEXTJS_SEPARATOR____"
context = {
**(extra_context or {}),
"django_nextjs__": {
"section2": SEPARATOR,
"section3": SEPARATOR,
"section4": SEPARATOR,
},
}

html = await sync_to_async(render_to_string)(template_name, context=context, request=request, using=using)

def _get_nextjs_response_headers(headers: MultiMapping[str], include_content_type: bool = False) -> Dict:
useful_header_keys = ["Location", "Vary"]
# Can't send "\n" in HTTP headers
items = html.replace("\n", " ").split(SEPARATOR)

if include_content_type:
useful_header_keys.append("Content-Type")
if len(items) < 4:
raise ValueError(
"You should put {{ block.super }} in your head and body blocks of" + f" '{template_name}' template."
)

return {key: headers[key] for key in useful_header_keys if key in headers}
return {"dj_pre_head": items[0], "dj_post_head": items[1], "dj_pre_body": items[2], "dj_post_body": items[3]}


async def _render_nextjs_page_to_string(
Expand All @@ -90,21 +122,26 @@ async def _render_nextjs_page_to_string(
using: Union[str, None] = None,
allow_redirects: bool = False,
headers: Union[Dict, None] = None,
include_content_type: bool = False,
) -> Tuple[str, int, Dict[str, str]]:
page_path = quote(request.path_info.lstrip("/"))
params = [(k, v) for k in request.GET.keys() for v in request.GET.getlist(k)]

rendered_template = (
await _render_extended_template(template_name=template_name, request=request, using=using)
if template_name
else {}
)

# Get HTML from Next.js server
async with aiohttp.ClientSession(
cookies=_get_nextjs_request_cookies(request),
headers=_get_nextjs_request_headers(request, headers),
headers=_get_nextjs_request_headers(request, {**(headers or {}), **rendered_template}),
) as session:
async with session.get(
f"{NEXTJS_SERVER_URL}/{page_path}", params=params, allow_redirects=allow_redirects
) as response:
html = await response.text()
response_headers = _get_nextjs_response_headers(response.headers, include_content_type)
response_headers = _get_nextjs_response_headers(response.headers)

# Apply template rendering (HTML customization) if template_name is provided
if template_name:
Expand Down Expand Up @@ -139,8 +176,6 @@ async def render_nextjs_page(
request: HttpRequest,
template_name: str = "",
context: Union[Dict, None] = None,
content_type: Union[str, None] = None,
override_status: Union[int, None] = None,
using: Union[str, None] = None,
allow_redirects: bool = False,
headers: Union[Dict, None] = None,
Expand All @@ -152,10 +187,8 @@ async def render_nextjs_page(
using=using,
allow_redirects=allow_redirects,
headers=headers,
include_content_type=content_type is None,
)
final_status = status if override_status is None else override_status
return HttpResponse(content, content_type, final_status, headers=response_headers)
return HttpResponse(content=content, status=status, headers=response_headers)


async def render_nextjs_page_to_string_async(*args, **kwargs):
Expand Down
14 changes: 14 additions & 0 deletions django_nextjs/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import typing

Key = typing.TypeVar("Key")
Value = typing.TypeVar("Value")


def filter_mapping_obj(
mapping_obj: typing.Mapping[Key, Value], *, selected_keys: typing.Iterable
) -> typing.Dict[Key, Value]:
"""
Selects the items in a mapping object (dict, etc.)
"""

return {key: mapping_obj[key] for key in selected_keys if key in mapping_obj}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "tests.settings"
DJANGO_SETTINGS_MODULE = "tests.sample.djproject.settings"
norecursedirs = ".git"
django_find_project = false
pythonpath = ["."]
Expand Down
31 changes: 31 additions & 0 deletions tests/sample/djproject/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os

from django.core.asgi import get_asgi_application
from django.urls import path, re_path

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djproject.settings")
django_asgi_app = get_asgi_application()

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf import settings

from django_nextjs.proxy import NextJSProxyHttpConsumer, NextJSProxyWebsocketConsumer

# put your custom routes here if you need
http_routes = [re_path(r"", django_asgi_app)]
websocket_routers = []

if settings.DEBUG:
http_routes.insert(0, re_path(r"^(?:_next|__next|next).*", NextJSProxyHttpConsumer.as_asgi()))
websocket_routers.insert(0, path("_next/webpack-hmr", NextJSProxyWebsocketConsumer.as_asgi()))


application = ProtocolTypeRouter(
{
# Django's ASGI application to handle traditional HTTP and websocket requests.
"http": URLRouter(http_routes),
"websocket": AuthMiddlewareStack(URLRouter(websocket_routers)),
# ...
}
)
19 changes: 9 additions & 10 deletions tests/settings.py → tests/sample/djproject/settings.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"

DEBUG = True
USE_TZ = False

INSTALLED_APPS = [
"daphne",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django_nextjs.apps.DjangoNextJSConfig",
]

MIDDLEWARE = [
Expand All @@ -22,12 +25,12 @@
"django.contrib.messages.middleware.MessageMiddleware",
]

# ROOT_URLCONF = "tests.urls"
ROOT_URLCONF = "djproject.urls"

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": [BASE_DIR / "djproject" / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
Expand All @@ -40,12 +43,8 @@
},
]

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
WSGI_APPLICATION = "djproject.wsgi.application"
ASGI_APPLICATION = "djproject.asgi.application"

STATIC_URL = "/static/"

Expand Down
15 changes: 15 additions & 0 deletions tests/sample/djproject/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends "django_nextjs/document_base.html" %}


{% block head %}
<title>head</title>
{{ block.super }}
<meta name="description" content="head">
{% endblock %}


{% block body %}
<span>pre_body_{{ request.path_info }}</span>
{{ block.super }}
<span>post_body_{{ request.path_info }}</span>
{% endblock %}
23 changes: 23 additions & 0 deletions tests/sample/djproject/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib import admin
from django.urls import include, path

from django_nextjs.render import render_nextjs_page


async def nextjs_page(request):
return await render_nextjs_page(request)


async def nextjs_page_with_template(request):
return await render_nextjs_page(request, "index.html")


urlpatterns = [
path("admin/", admin.site.urls),
path("", nextjs_page),
path("app", nextjs_page_with_template),
path("app/second", nextjs_page_with_template),
path("page", nextjs_page_with_template),
path("page/second", nextjs_page_with_template),
path("", include("django_nextjs.urls")),
]
16 changes: 16 additions & 0 deletions tests/sample/djproject/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for sample project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djproject.settings")

application = get_wsgi_application()
22 changes: 22 additions & 0 deletions tests/sample/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djproject.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == "__main__":
main()
35 changes: 35 additions & 0 deletions tests/sample/next/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
12 changes: 12 additions & 0 deletions tests/sample/next/app/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from "next/link";

export default function Home() {
return (
<div>
<div>/app</div>
<Link href="/app/second" id="2">
/app/second
</Link>
</div>
);
}
12 changes: 12 additions & 0 deletions tests/sample/next/app/app/second/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from "next/link";

export default function Home() {
return (
<div>
<div>/app/second</div>
<Link href="/app" id="1">
/app
</Link>
</div>
);
}
Loading

0 comments on commit e15d0de

Please sign in to comment.