|
1 | 1 | from http import HTTPStatus
|
2 |
| -from django.http import HttpResponse, JsonResponse |
3 |
| -from django.shortcuts import render as base_render |
| 2 | +from django.template.loader import render_to_string |
| 3 | +from django.http import HttpResponse |
4 | 4 | from .settings import settings
|
5 | 5 | from json import dumps as json_encode
|
6 | 6 | from functools import wraps
|
7 | 7 | import requests
|
8 | 8 | from .prop_classes import IgnoreOnFirstLoadProp, DeferredProp, MergeableProp
|
| 9 | +from .helpers import deep_transform_callables, validate_type |
9 | 10 |
|
10 | 11 | INERTIA_REQUEST_ENCRYPT_HISTORY = "_inertia_encrypt_history"
|
11 | 12 | INERTIA_SESSION_CLEAR_HISTORY = "_inertia_clear_history"
|
12 | 13 |
|
13 |
| -def render(request, component, props={}, template_data={}): |
14 |
| - def is_a_partial_render(): |
15 |
| - return 'X-Inertia-Partial-Data' in request.headers and request.headers.get('X-Inertia-Partial-Component', '') == component |
| 14 | +INERTIA_TEMPLATE = 'inertia.html' |
| 15 | +INERTIA_SSR_TEMPLATE = 'inertia_ssr.html' |
16 | 16 |
|
17 |
| - def partial_keys(): |
18 |
| - return request.headers.get('X-Inertia-Partial-Data', '').split(',') |
| 17 | +class InertiaRequest: |
| 18 | + def __init__(self, request): |
| 19 | + self.request = request |
| 20 | + |
| 21 | + def __getattr__(self, name): |
| 22 | + return getattr(self.request, name) |
| 23 | + |
| 24 | + @property |
| 25 | + def headers(self): |
| 26 | + return self.request.headers |
| 27 | + |
| 28 | + @property |
| 29 | + def inertia(self): |
| 30 | + return self.request.inertia.all() if hasattr(self.request, 'inertia') else {} |
| 31 | + |
| 32 | + def is_a_partial_render(self, component): |
| 33 | + return 'X-Inertia-Partial-Data' in self.headers and self.headers.get('X-Inertia-Partial-Component', '') == component |
19 | 34 |
|
20 |
| - def deep_transform_callables(prop): |
21 |
| - if not isinstance(prop, dict): |
22 |
| - return prop() if callable(prop) else prop |
23 |
| - |
24 |
| - for key in list(prop.keys()): |
25 |
| - prop[key] = deep_transform_callables(prop[key]) |
| 35 | + def partial_keys(self): |
| 36 | + return self.headers.get('X-Inertia-Partial-Data', '').split(',') |
| 37 | + |
| 38 | + def reset_keys(self): |
| 39 | + return self.headers.get('X-Inertia-Reset', '').split(',') |
| 40 | + |
| 41 | + def is_inertia(self): |
| 42 | + return 'X-Inertia' in self.headers |
| 43 | + |
| 44 | + def should_encrypt_history(self): |
| 45 | + return validate_type( |
| 46 | + getattr(self.request, INERTIA_REQUEST_ENCRYPT_HISTORY, settings.INERTIA_ENCRYPT_HISTORY), |
| 47 | + expected_type=bool, |
| 48 | + name="encrypt_history" |
| 49 | + ) |
| 50 | + |
| 51 | +class BaseInertiaResponseMixin: |
| 52 | + def page_data(self): |
| 53 | + clear_history = validate_type( |
| 54 | + self.request.session.pop(INERTIA_SESSION_CLEAR_HISTORY, False), |
| 55 | + expected_type=bool, |
| 56 | + name="clear_history" |
| 57 | + ) |
| 58 | + |
| 59 | + _page = { |
| 60 | + 'component': self.component, |
| 61 | + 'props': self.build_props(), |
| 62 | + 'url': self.request.get_full_path(), |
| 63 | + 'version': settings.INERTIA_VERSION, |
| 64 | + 'encryptHistory': self.request.should_encrypt_history(), |
| 65 | + 'clearHistory': clear_history, |
| 66 | + } |
26 | 67 |
|
27 |
| - return prop |
| 68 | + _deferred_props = self.build_deferred_props() |
| 69 | + if _deferred_props: |
| 70 | + _page['deferredProps'] = _deferred_props |
| 71 | + |
| 72 | + _merge_props = self.build_merge_props() |
| 73 | + if _merge_props: |
| 74 | + _page['mergeProps'] = _merge_props |
| 75 | + |
| 76 | + return _page |
28 | 77 |
|
29 |
| - def build_props(): |
| 78 | + def build_props(self): |
30 | 79 | _props = {
|
31 |
| - **(request.inertia.all() if hasattr(request, 'inertia') else {}), |
32 |
| - **props, |
| 80 | + **(self.request.inertia), |
| 81 | + **self.props, |
33 | 82 | }
|
34 | 83 |
|
35 | 84 | for key in list(_props.keys()):
|
36 |
| - if is_a_partial_render(): |
37 |
| - if key not in partial_keys(): |
| 85 | + if self.request.is_a_partial_render(self.component): |
| 86 | + if key not in self.request.partial_keys(): |
38 | 87 | del _props[key]
|
39 | 88 | else:
|
40 | 89 | if isinstance(_props[key], IgnoreOnFirstLoadProp):
|
41 | 90 | del _props[key]
|
42 | 91 |
|
43 | 92 | return deep_transform_callables(_props)
|
44 | 93 |
|
45 |
| - def build_deferred_props(): |
46 |
| - if is_a_partial_render(): |
| 94 | + def build_deferred_props(self): |
| 95 | + if self.request.is_a_partial_render(self.component): |
47 | 96 | return None
|
48 | 97 |
|
49 | 98 | _deferred_props = {}
|
50 |
| - for key, prop in props.items(): |
| 99 | + for key, prop in self.props.items(): |
51 | 100 | if isinstance(prop, DeferredProp):
|
52 | 101 | _deferred_props.setdefault(prop.group, []).append(key)
|
53 | 102 |
|
54 | 103 | return _deferred_props
|
55 |
| - |
56 |
| - def build_merge_props(): |
57 |
| - reset_keys = request.headers.get('X-Inertia-Reset', '').split(',') |
58 | 104 |
|
| 105 | + def build_merge_props(self): |
59 | 106 | return [
|
60 | 107 | key
|
61 |
| - for key, prop in props.items() |
| 108 | + for key, prop in self.props.items() |
62 | 109 | if (
|
63 | 110 | isinstance(prop, MergeableProp)
|
64 | 111 | and prop.should_merge()
|
65 |
| - and key not in reset_keys |
| 112 | + and key not in self.request.reset_keys() |
66 | 113 | )
|
67 | 114 | ]
|
68 |
| - |
69 |
| - def render_ssr(): |
70 |
| - data = json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER) |
71 |
| - response = requests.post( |
72 |
| - f"{settings.INERTIA_SSR_URL}/render", |
73 |
| - data=data, |
74 |
| - headers={"Content-Type": "application/json"}, |
| 115 | + |
| 116 | + def build_first_load(self, data): |
| 117 | + context, template = self.build_first_load_context_and_template(data) |
| 118 | + |
| 119 | + return render_to_string( |
| 120 | + template, |
| 121 | + { |
| 122 | + 'inertia_layout': settings.INERTIA_LAYOUT, |
| 123 | + **context, |
| 124 | + }, |
| 125 | + self.request, |
| 126 | + using=None, |
75 | 127 | )
|
76 |
| - response.raise_for_status() |
77 |
| - return base_render(request, 'inertia_ssr.html', { |
78 |
| - 'inertia_layout': settings.INERTIA_LAYOUT, |
79 |
| - **response.json() |
80 |
| - }) |
81 | 128 |
|
82 |
| - def page_data(): |
83 |
| - encrypt_history = getattr(request, INERTIA_REQUEST_ENCRYPT_HISTORY, settings.INERTIA_ENCRYPT_HISTORY) |
84 |
| - if not isinstance(encrypt_history, bool): |
85 |
| - raise TypeError(f"Expected boolean for encrypt_history, got {type(encrypt_history).__name__}") |
| 129 | + |
| 130 | + def build_first_load_context_and_template(self, data): |
| 131 | + if settings.INERTIA_SSR_ENABLED: |
| 132 | + try: |
| 133 | + response = requests.post( |
| 134 | + f"{settings.INERTIA_SSR_URL}/render", |
| 135 | + data=data, |
| 136 | + headers={"Content-Type": "application/json"}, |
| 137 | + ) |
| 138 | + response.raise_for_status() |
| 139 | + return { |
| 140 | + **response.json(), |
| 141 | + **self.template_data, |
| 142 | + }, INERTIA_SSR_TEMPLATE |
| 143 | + except Exception: |
| 144 | + pass |
| 145 | + |
| 146 | + return { |
| 147 | + 'page': data, |
| 148 | + **(self.template_data), |
| 149 | + }, INERTIA_TEMPLATE |
86 | 150 |
|
87 |
| - clear_history = request.session.pop(INERTIA_SESSION_CLEAR_HISTORY, False) |
88 |
| - if not isinstance(clear_history, bool): |
89 |
| - raise TypeError(f"Expected boolean for clear_history, got {type(clear_history).__name__}") |
90 | 151 |
|
91 |
| - _page = { |
92 |
| - 'component': component, |
93 |
| - 'props': build_props(), |
94 |
| - 'url': request.build_absolute_uri(), |
95 |
| - 'version': settings.INERTIA_VERSION, |
96 |
| - 'encryptHistory': encrypt_history, |
97 |
| - 'clearHistory': clear_history, |
98 |
| - } |
| 152 | +class InertiaResponse(BaseInertiaResponseMixin, HttpResponse): |
| 153 | + json_encoder = settings.INERTIA_JSON_ENCODER |
99 | 154 |
|
100 |
| - _deferred_props = build_deferred_props() |
101 |
| - if _deferred_props: |
102 |
| - _page['deferredProps'] = _deferred_props |
103 |
| - |
104 |
| - _merge_props = build_merge_props() |
105 |
| - if _merge_props: |
106 |
| - _page['mergeProps'] = _merge_props |
107 |
| - |
108 |
| - return _page |
| 155 | + def __init__(self, request, component, props=None, template_data=None, headers=None, *args, **kwargs): |
| 156 | + self.request = InertiaRequest(request) |
| 157 | + self.component = component |
| 158 | + self.props = props or {} |
| 159 | + self.template_data = template_data or {} |
| 160 | + _headers = headers or {} |
| 161 | + |
| 162 | + data = json_encode(self.page_data(), cls=self.json_encoder) |
109 | 163 |
|
110 |
| - if 'X-Inertia' in request.headers: |
111 |
| - return JsonResponse( |
112 |
| - data=page_data(), |
113 |
| - headers={ |
| 164 | + if self.request.is_inertia(): |
| 165 | + _headers = { |
| 166 | + **_headers, |
114 | 167 | 'Vary': 'X-Inertia',
|
115 | 168 | 'X-Inertia': 'true',
|
116 |
| - }, |
117 |
| - encoder=settings.INERTIA_JSON_ENCODER, |
| 169 | + 'Content-Type': 'application/json', |
| 170 | + } |
| 171 | + content = data |
| 172 | + else: |
| 173 | + content = self.build_first_load(data) |
| 174 | + |
| 175 | + super().__init__( |
| 176 | + content=content, |
| 177 | + headers=_headers, |
| 178 | + *args, |
| 179 | + **kwargs, |
118 | 180 | )
|
119 | 181 |
|
120 |
| - if settings.INERTIA_SSR_ENABLED: |
121 |
| - try: |
122 |
| - return render_ssr() |
123 |
| - except Exception: |
124 |
| - pass |
125 |
| - |
126 |
| - return base_render(request, 'inertia.html', { |
127 |
| - 'inertia_layout': settings.INERTIA_LAYOUT, |
128 |
| - 'page': json_encode(page_data(), cls=settings.INERTIA_JSON_ENCODER), |
129 |
| - **template_data, |
130 |
| - }) |
| 182 | +def render(request, component, props=None, template_data=None): |
| 183 | + return InertiaResponse( |
| 184 | + request, |
| 185 | + component, |
| 186 | + props or {}, |
| 187 | + template_data or {} |
| 188 | + ) |
131 | 189 |
|
132 | 190 | def location(location):
|
133 | 191 | return HttpResponse('', status=HTTPStatus.CONFLICT, headers={
|
|
0 commit comments