Skip to content

Commit

Permalink
Finish handling direct view.
Browse files Browse the repository at this point in the history
  • Loading branch information
adamghill committed Dec 18, 2021
1 parent 42df86b commit cfb48e5
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 9 deletions.
18 changes: 15 additions & 3 deletions django_unicorn/components/unicorn_template_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django_unicorn.utils import sanitize_html

from ..decorators import timed
from ..errors import MissingComponentElement, MissingComponentViewElement
from ..utils import generate_checksum


Expand Down Expand Up @@ -132,15 +133,26 @@ def _desoupify(soup):

def get_root_element(soup: BeautifulSoup) -> Tag:
"""
Gets the first tag element.
Gets the first tag element for the component or the first element with a `unicorn:view` attribute for a direct view.
Returns:
BeautifulSoup tag element.
Raises an Exception if an element cannot be found.
Raises `Exception` if an element cannot be found.
"""

for element in soup.contents:
if isinstance(element, Tag) and element.name:
if element.name == "html":
view_element = element.find_next(attrs={"unicorn:view": True})

if not view_element:
raise MissingComponentViewElement(
"An element with an `unicorn:view` attribute is required for a direct view"
)

return view_element

return element

raise Exception("No root element found")
raise MissingComponentElement("No root element for the component was found")
17 changes: 15 additions & 2 deletions django_unicorn/components/unicorn_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from django.http import HttpRequest
from django.utils.decorators import classonlymethod
from django.views.generic.base import TemplateView

import shortuuid
Expand Down Expand Up @@ -299,18 +300,25 @@ def called(self, name, args):
pass

@timed
def render(self, init_js=False, extra_context=None) -> str:
def render(self, init_js=False, extra_context=None, request=None) -> str:
"""
Renders a UnicornView component with the public properties available. Delegates to a
UnicornTemplateResponse to actually render a response.
Args:
param init_js: Whether or not to include the Javascript required to initialize the component.
param extra_context:
param request: Set the `request` for rendering. Usually it will be in the context,
but it is missing when the component is re-rendered as a direct view, so it needs
to be set explicitly.
"""

if extra_context is not None:
self.extra_context = extra_context

if request:
self.request = request

response = self.render_to_response(
context=self.get_context_data(),
component=self,
Expand Down Expand Up @@ -341,8 +349,13 @@ def dispatch(self, request, *args, **kwargs):
Called by the `as_view` class method when utilizing a component directly as a view.
"""

self.mount()
self.hydrate()

return self.render_to_response(
context=self.get_context_data(), component=self, init_js=True,
context=self.get_context_data(),
component=self,
init_js=True,
)

@timed
Expand Down
8 changes: 8 additions & 0 deletions django_unicorn/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ class ComponentLoadError(Exception):

class RenderNotModified(Exception):
pass


class MissingComponentElement(Exception):
pass


class MissingComponentViewElement(Exception):
pass
3 changes: 2 additions & 1 deletion django_unicorn/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ def _process_component_request(
else:
component.validate(model_names=list(updated_data.keys()))

rendered_component = component.render()
# Pass the current request so that it can be used inside the component template
rendered_component = component.render(request=request)
component.rendered(rendered_component)

cache = caches[get_cache_alias()]
Expand Down
24 changes: 23 additions & 1 deletion tests/components/test_unicorn_template_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
UnicornTemplateResponse,
get_root_element,
)
from django_unicorn.errors import MissingComponentElement, MissingComponentViewElement


def test_get_root_element():
Expand Down Expand Up @@ -43,12 +44,33 @@ def test_get_root_element_no_element():
component_html = "\n"
soup = BeautifulSoup(component_html, features="html.parser")

with pytest.raises(Exception):
with pytest.raises(MissingComponentElement):
actual = get_root_element(soup)

assert str(actual) == expected


def test_get_root_element_direct_view():
# Annoyingly beautifulsoup adds a blank string on the attribute
expected = '<div unicorn:view="">test</div>'

component_html = (
"<html><head></head>≤body><div unicorn:view>test</div></body></html>"
)
soup = BeautifulSoup(component_html, features="html.parser")
actual = get_root_element(soup)

assert str(actual) == expected


def test_get_root_element_direct_view_no_view():
component_html = "<html><head></head>≤body><div>test</div></body></html>"
soup = BeautifulSoup(component_html, features="html.parser")

with pytest.raises(MissingComponentViewElement):
get_root_element(soup)


def test_desoupify():
html = "<div>&lt;a&gt;&lt;style&gt;@keyframes x{}&lt;/style&gt;&lt;a style=&quot;animation-name:x&quot; onanimationend=&quot;alert(1)&quot;&gt;&lt;/a&gt;!\n</div>\n\n<script type=\"application/javascript\">\n window.addEventListener('DOMContentLoaded', (event) => {\n Unicorn.addEventListener('updated', (component) => console.log('got updated', component));\n });\n</script>"
expected = "<div>&lt;a&gt;&lt;style&gt;@keyframes x{}&lt;/style&gt;&lt;a style=\"animation-name:x\" onanimationend=\"alert(1)\"&gt;&lt;/a&gt;!\n</div>\n<script type=\"application/javascript\">\n window.addEventListener('DOMContentLoaded', (event) => {\n Unicorn.addEventListener('updated', (component) => console.log('got updated', component));\n });\n</script>"
Expand Down
3 changes: 1 addition & 2 deletions tests/templatetags/test_unicorn_render.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import re

from django.http.request import HttpRequest
from django.template import Context, RequestContext
from django.template import Context
from django.template.base import Token, TokenType

import pytest
Expand Down

0 comments on commit cfb48e5

Please sign in to comment.