Skip to content

Commit 036cd3f

Browse files
committed
Refactor and split up components code.
1 parent ed3ad22 commit 036cd3f

File tree

7 files changed

+207
-191
lines changed

7 files changed

+207
-191
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .unicorn_view import *
2+
from .updaters import *
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class UnicornField:
2+
"""
3+
Base class to provide a way to serialize a component field quickly.
4+
"""
5+
6+
def to_json(self):
7+
return self.__dict__
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import logging
2+
3+
from django.template.response import TemplateResponse
4+
from django.utils.safestring import mark_safe
5+
6+
import orjson
7+
from bs4 import BeautifulSoup
8+
from bs4.element import Tag
9+
from bs4.formatter import HTMLFormatter
10+
11+
from ..decorators import timed
12+
from ..utils import generate_checksum
13+
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class UnsortedAttributes(HTMLFormatter):
19+
"""
20+
Prevent beautifulsoup from re-ordering attributes.
21+
"""
22+
23+
def attributes(self, tag: Tag):
24+
for k, v in tag.attrs.items():
25+
yield k, v
26+
27+
28+
class UnicornTemplateResponse(TemplateResponse):
29+
def __init__(
30+
self,
31+
template,
32+
request,
33+
context=None,
34+
content_type=None,
35+
status=None,
36+
charset=None,
37+
using=None,
38+
component=None,
39+
init_js=False,
40+
**kwargs,
41+
):
42+
super().__init__(
43+
template=template,
44+
request=request,
45+
context=context,
46+
content_type=content_type,
47+
status=status,
48+
charset=charset,
49+
using=using,
50+
)
51+
52+
self.component = component
53+
self.init_js = init_js
54+
55+
@timed
56+
def render(self):
57+
response = super().render()
58+
59+
if not self.component or not self.component.component_id:
60+
return response
61+
62+
content = response.content.decode("utf-8")
63+
64+
frontend_context_variables = self.component.get_frontend_context_variables()
65+
frontend_context_variables_dict = orjson.loads(frontend_context_variables)
66+
checksum = generate_checksum(orjson.dumps(frontend_context_variables_dict))
67+
68+
soup = BeautifulSoup(content, features="html.parser")
69+
root_element = UnicornTemplateResponse._get_root_element(soup)
70+
root_element["unicorn:id"] = self.component.component_id
71+
root_element["unicorn:name"] = self.component.component_name
72+
root_element["unicorn:key"] = self.component.component_key
73+
root_element["unicorn:checksum"] = checksum
74+
75+
if self.init_js:
76+
init = {
77+
"id": self.component.component_id,
78+
"name": self.component.component_name,
79+
"key": self.component.component_key,
80+
"data": orjson.loads(frontend_context_variables),
81+
}
82+
init = orjson.dumps(init).decode("utf-8")
83+
init_script = f"Unicorn.componentInit({init});"
84+
85+
if self.component.parent:
86+
self.component._init_script = init_script
87+
else:
88+
for child in self.component.children:
89+
init_script = f"{init_script} {child._init_script}"
90+
91+
script_tag = soup.new_tag("script")
92+
script_tag["type"] = "module"
93+
script_tag.string = f"if (typeof Unicorn === 'undefined') {{ console.error('Unicorn is missing. Do you need {{% load unicorn %}} or {{% unicorn-scripts %}}?') }} else {{ {init_script} }}"
94+
root_element.insert_after(script_tag)
95+
96+
rendered_template = UnicornTemplateResponse._desoupify(soup)
97+
rendered_template = mark_safe(rendered_template)
98+
99+
response.content = rendered_template
100+
101+
return response
102+
103+
@staticmethod
104+
def _get_root_element(soup: BeautifulSoup) -> Tag:
105+
"""
106+
Gets the first div element.
107+
108+
Returns:
109+
BeautifulSoup element.
110+
111+
Raises an Exception if a div cannot be found.
112+
"""
113+
for element in soup.contents:
114+
if element.name:
115+
return element
116+
117+
raise Exception("No root element found")
118+
119+
@staticmethod
120+
def _desoupify(soup):
121+
soup.smooth()
122+
return soup.encode(formatter=UnsortedAttributes()).decode("utf-8")

django_unicorn/components.py renamed to django_unicorn/components/unicorn_view.py

Lines changed: 6 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,16 @@
77
from django.core.exceptions import ImproperlyConfigured
88
from django.db.models import Model
99
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
1310
from django.views.generic.base import TemplateView
1411

15-
import orjson
16-
from bs4 import BeautifulSoup
17-
from bs4.element import Tag
18-
from bs4.formatter import HTMLFormatter
1912
from cachetools.lru import LRUCache
2013

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
2520

2621

2722
logger = logging.getLogger(__name__)
@@ -36,85 +31,6 @@
3631
constructed_views_cache = LRUCache(maxsize=100)
3732

3833

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-
11834
def convert_to_snake_case(s: str) -> str:
11935
# TODO: Better handling of dash->snake
12036
return s.replace("-", "_")
@@ -203,103 +119,6 @@ def construct_component(
203119
return component
204120

205121

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-
303122
class UnicornView(TemplateView):
304123
response_class = UnicornTemplateResponse
305124
component_name: str = ""

0 commit comments

Comments
 (0)