|
7 | 7 | from django.core.exceptions import ImproperlyConfigured |
8 | 8 | from django.db.models import Model |
9 | 9 | from django.http import HttpRequest |
10 | | -from django.http.response import HttpResponseRedirect |
11 | | -from django.template.response import TemplateResponse |
12 | | -from django.utils.safestring import mark_safe |
13 | 10 | from django.views.generic.base import TemplateView |
14 | 11 |
|
15 | | -import orjson |
16 | | -from bs4 import BeautifulSoup |
17 | | -from bs4.element import Tag |
18 | | -from bs4.formatter import HTMLFormatter |
19 | 12 | from cachetools.lru import LRUCache |
20 | 13 |
|
21 | | -from . import serializer |
22 | | -from .decorators import timed |
23 | | -from .settings import get_setting |
24 | | -from .utils import generate_checksum |
| 14 | +from .. import serializer |
| 15 | +from ..decorators import timed |
| 16 | +from ..errors import ComponentLoadError |
| 17 | +from ..settings import get_setting |
| 18 | +from .fields import UnicornField |
| 19 | +from .unicorn_template_response import UnicornTemplateResponse |
25 | 20 |
|
26 | 21 |
|
27 | 22 | logger = logging.getLogger(__name__) |
|
36 | 31 | constructed_views_cache = LRUCache(maxsize=100) |
37 | 32 |
|
38 | 33 |
|
39 | | -class UnicornField: |
40 | | - """ |
41 | | - Base class to provide a way to serialize a component field quickly. |
42 | | - """ |
43 | | - |
44 | | - def to_json(self): |
45 | | - return self.__dict__ |
46 | | - |
47 | | - |
48 | | -class Update: |
49 | | - """ |
50 | | - Base class for updaters. |
51 | | - """ |
52 | | - |
53 | | - def to_json(self): |
54 | | - return self.__dict__ |
55 | | - |
56 | | - |
57 | | -class HashUpdate(Update): |
58 | | - """ |
59 | | - Updates the current URL hash from an action method. |
60 | | - """ |
61 | | - |
62 | | - def __init__(self, hash: str): |
63 | | - """ |
64 | | - Args: |
65 | | - param hash: The hash to change. Example: `#model-123`. |
66 | | - """ |
67 | | - self.hash = hash |
68 | | - |
69 | | - |
70 | | -class LocationUpdate(Update): |
71 | | - """ |
72 | | - Updates the current URL from an action method. |
73 | | - """ |
74 | | - |
75 | | - def __init__(self, redirect: HttpResponseRedirect, title: str = None): |
76 | | - """ |
77 | | - Args: |
78 | | - param redirect: The redirect that contains the URL to redirect to. |
79 | | - param title: The new title of the page. Optional. |
80 | | - """ |
81 | | - self.redirect = redirect |
82 | | - self.title = title |
83 | | - |
84 | | - |
85 | | -class PollUpdate(Update): |
86 | | - """ |
87 | | - Updates the current poll from an action method. |
88 | | - """ |
89 | | - |
90 | | - def __init__(self, timing: int = None, method: str = None, disable: bool = False): |
91 | | - """ |
92 | | - Args: |
93 | | - param timing: The timing that should be used for the poll. Optional. Defaults to `None` |
94 | | - which keeps the existing timing. |
95 | | - param method: The method that should be used for the poll. Optional. Defaults to `None` |
96 | | - which keeps the existing method. |
97 | | - param disable: Whether to disable the poll or not not. Optional. Defaults to `False`. |
98 | | - """ |
99 | | - self.timing = timing |
100 | | - self.method = method |
101 | | - self.disable = disable |
102 | | - |
103 | | - |
104 | | -class ComponentLoadError(Exception): |
105 | | - pass |
106 | | - |
107 | | - |
108 | | -class UnsortedAttributes(HTMLFormatter): |
109 | | - """ |
110 | | - Prevent beautifulsoup from re-ordering attributes. |
111 | | - """ |
112 | | - |
113 | | - def attributes(self, tag: Tag): |
114 | | - for k, v in tag.attrs.items(): |
115 | | - yield k, v |
116 | | - |
117 | | - |
118 | 34 | def convert_to_snake_case(s: str) -> str: |
119 | 35 | # TODO: Better handling of dash->snake |
120 | 36 | return s.replace("-", "_") |
@@ -203,103 +119,6 @@ def construct_component( |
203 | 119 | return component |
204 | 120 |
|
205 | 121 |
|
206 | | -class UnicornTemplateResponse(TemplateResponse): |
207 | | - def __init__( |
208 | | - self, |
209 | | - template, |
210 | | - request, |
211 | | - context=None, |
212 | | - content_type=None, |
213 | | - status=None, |
214 | | - charset=None, |
215 | | - using=None, |
216 | | - component=None, |
217 | | - init_js=False, |
218 | | - **kwargs, |
219 | | - ): |
220 | | - super().__init__( |
221 | | - template=template, |
222 | | - request=request, |
223 | | - context=context, |
224 | | - content_type=content_type, |
225 | | - status=status, |
226 | | - charset=charset, |
227 | | - using=using, |
228 | | - ) |
229 | | - |
230 | | - self.component = component |
231 | | - self.init_js = init_js |
232 | | - |
233 | | - @timed |
234 | | - def render(self): |
235 | | - response = super().render() |
236 | | - |
237 | | - if not self.component or not self.component.component_id: |
238 | | - return response |
239 | | - |
240 | | - content = response.content.decode("utf-8") |
241 | | - |
242 | | - frontend_context_variables = self.component.get_frontend_context_variables() |
243 | | - frontend_context_variables_dict = orjson.loads(frontend_context_variables) |
244 | | - checksum = generate_checksum(orjson.dumps(frontend_context_variables_dict)) |
245 | | - |
246 | | - soup = BeautifulSoup(content, features="html.parser") |
247 | | - root_element = UnicornTemplateResponse._get_root_element(soup) |
248 | | - root_element["unicorn:id"] = self.component.component_id |
249 | | - root_element["unicorn:name"] = self.component.component_name |
250 | | - root_element["unicorn:key"] = self.component.component_key |
251 | | - root_element["unicorn:checksum"] = checksum |
252 | | - |
253 | | - if self.init_js: |
254 | | - init = { |
255 | | - "id": self.component.component_id, |
256 | | - "name": self.component.component_name, |
257 | | - "key": self.component.component_key, |
258 | | - "data": orjson.loads(frontend_context_variables), |
259 | | - } |
260 | | - init = orjson.dumps(init).decode("utf-8") |
261 | | - init_script = f"Unicorn.componentInit({init});" |
262 | | - |
263 | | - if self.component.parent: |
264 | | - self.component._init_script = init_script |
265 | | - else: |
266 | | - for child in self.component.children: |
267 | | - init_script = f"{init_script} {child._init_script}" |
268 | | - |
269 | | - script_tag = soup.new_tag("script") |
270 | | - script_tag["type"] = "module" |
271 | | - script_tag.string = f"if (typeof Unicorn === 'undefined') {{ console.error('Unicorn is missing. Do you need {{% load unicorn %}} or {{% unicorn-scripts %}}?') }} else {{ {init_script} }}" |
272 | | - root_element.insert_after(script_tag) |
273 | | - |
274 | | - rendered_template = UnicornTemplateResponse._desoupify(soup) |
275 | | - rendered_template = mark_safe(rendered_template) |
276 | | - |
277 | | - response.content = rendered_template |
278 | | - |
279 | | - return response |
280 | | - |
281 | | - @staticmethod |
282 | | - def _get_root_element(soup: BeautifulSoup) -> Tag: |
283 | | - """ |
284 | | - Gets the first div element. |
285 | | -
|
286 | | - Returns: |
287 | | - BeautifulSoup element. |
288 | | - |
289 | | - Raises an Exception if a div cannot be found. |
290 | | - """ |
291 | | - for element in soup.contents: |
292 | | - if element.name: |
293 | | - return element |
294 | | - |
295 | | - raise Exception("No root element found") |
296 | | - |
297 | | - @staticmethod |
298 | | - def _desoupify(soup): |
299 | | - soup.smooth() |
300 | | - return soup.encode(formatter=UnsortedAttributes()).decode("utf-8") |
301 | | - |
302 | | - |
303 | 122 | class UnicornView(TemplateView): |
304 | 123 | response_class = UnicornTemplateResponse |
305 | 124 | component_name: str = "" |
|
0 commit comments