mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-07-08 01:55:01 +00:00
removed cotton copy
This commit is contained in:
parent
419292664c
commit
c3431c6241
40 changed files with 0 additions and 1234 deletions
|
@ -1,304 +0,0 @@
|
|||
import warnings
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.template.loaders.base import Loader as BaseLoader
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.template import TemplateDoesNotExist
|
||||
from bs4.formatter import HTMLFormatter
|
||||
from django.utils._os import safe_join
|
||||
from django.template import Template
|
||||
from django.core.cache import cache
|
||||
from django.template import Origin
|
||||
from django.conf import settings
|
||||
|
||||
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||
|
||||
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||
|
||||
|
||||
class Loader(BaseLoader):
|
||||
is_usable = True
|
||||
|
||||
def __init__(self, engine, dirs=None):
|
||||
super().__init__(engine)
|
||||
self.cache_handler = CottonTemplateCacheHandler()
|
||||
self.template_processor = CottonTemplateProcessor()
|
||||
self.dirs = dirs
|
||||
|
||||
def get_contents(self, origin):
|
||||
# check if file exists, whilst getting the mtime for cache key
|
||||
try:
|
||||
mtime = os.path.getmtime(origin.name)
|
||||
except FileNotFoundError:
|
||||
raise TemplateDoesNotExist(origin)
|
||||
|
||||
cache_key = self.cache_handler.get_cache_key(origin.template_name, mtime)
|
||||
cached_content = self.cache_handler.get_cached_template(cache_key)
|
||||
|
||||
if cached_content is not None:
|
||||
return cached_content
|
||||
|
||||
template_string = self._get_template_string(origin.name)
|
||||
compiled_template = self.template_processor.process(
|
||||
template_string, origin.template_name
|
||||
)
|
||||
|
||||
self.cache_handler.cache_template(cache_key, compiled_template)
|
||||
|
||||
return compiled_template
|
||||
|
||||
def get_template_from_string(self, template_string):
|
||||
"""Create and return a Template object from a string. Used primarily for testing."""
|
||||
return Template(template_string, engine=self.engine)
|
||||
|
||||
def _get_template_string(self, template_name):
|
||||
try:
|
||||
with open(template_name, "r") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
raise TemplateDoesNotExist(template_name)
|
||||
|
||||
def get_dirs(self):
|
||||
return self.dirs if self.dirs is not None else self.engine.dirs
|
||||
|
||||
def get_template_sources(self, template_name):
|
||||
"""Return an Origin object pointing to an absolute path in each directory
|
||||
in template_dirs. For security reasons, if a path doesn't lie inside
|
||||
one of the template_dirs it is excluded from the result set."""
|
||||
for template_dir in self.get_dirs():
|
||||
try:
|
||||
name = safe_join(template_dir, template_name)
|
||||
except SuspiciousFileOperation:
|
||||
# The joined path was located outside of this template_dir
|
||||
# (it might be inside another one, so this isn't fatal).
|
||||
continue
|
||||
|
||||
yield Origin(
|
||||
name=name,
|
||||
template_name=template_name,
|
||||
loader=self,
|
||||
)
|
||||
|
||||
|
||||
class UnsortedAttributes(HTMLFormatter):
|
||||
"""This keeps BS4 from re-ordering attributes"""
|
||||
|
||||
def attributes(self, tag):
|
||||
for k, v in tag.attrs.items():
|
||||
yield k, v
|
||||
|
||||
|
||||
class CottonTemplateProcessor:
|
||||
DJANGO_SYNTAX_PLACEHOLDER_PREFIX = "__django_syntax__"
|
||||
COTTON_VERBATIM_PATTERN = re.compile(
|
||||
r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}", re.DOTALL
|
||||
)
|
||||
DJANGO_TAG_PATTERN = re.compile(r"\{%.*?%\}")
|
||||
DJANGO_VAR_PATTERN = re.compile(r"\{\{.*?\}\}")
|
||||
|
||||
def __init__(self):
|
||||
self.django_syntax_placeholders = []
|
||||
|
||||
def process(self, content, component_key):
|
||||
content = self._replace_syntax_with_placeholders(content)
|
||||
content = self._compile_cotton_to_django(content, component_key)
|
||||
content = self._replace_placeholders_with_syntax(content)
|
||||
return self._revert_bs4_attribute_empty_attribute_fixing(content)
|
||||
|
||||
def _replace_syntax_with_placeholders(self, content):
|
||||
"""# replace {% ... %} and {{ ... }} with placeholders so they dont get touched
|
||||
or encoded by bs4. Store them to later switch them back in after transformation.
|
||||
"""
|
||||
self.django_syntax_placeholders = []
|
||||
|
||||
def replace_pattern(pattern, replacement_func):
|
||||
return pattern.sub(replacement_func, content)
|
||||
|
||||
def replace_cotton_verbatim(match):
|
||||
inner_content = match.group(1)
|
||||
self.django_syntax_placeholders.append(inner_content)
|
||||
return f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
|
||||
|
||||
def replace_django_syntax(match):
|
||||
self.django_syntax_placeholders.append(match.group(0))
|
||||
return f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
|
||||
|
||||
# Replace cotton_verbatim blocks
|
||||
content = replace_pattern(self.COTTON_VERBATIM_PATTERN, replace_cotton_verbatim)
|
||||
|
||||
# Replace {% ... %}
|
||||
content = replace_pattern(self.DJANGO_TAG_PATTERN, replace_django_syntax)
|
||||
|
||||
# Replace {{ ... }}
|
||||
content = replace_pattern(self.DJANGO_VAR_PATTERN, replace_django_syntax)
|
||||
|
||||
return content
|
||||
|
||||
def _compile_cotton_to_django(self, html_content, component_key):
|
||||
"""Convert cotton <c-* syntax to {%."""
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
|
||||
# check if soup contains a 'c-vars' tag
|
||||
if cvars_el := soup.find("c-vars"):
|
||||
soup = self._wrap_with_cotton_vars_frame(soup, cvars_el)
|
||||
|
||||
self._transform_components(soup, component_key)
|
||||
|
||||
return str(soup.encode(formatter=UnsortedAttributes()).decode("utf-8"))
|
||||
|
||||
def _replace_placeholders_with_syntax(self, content):
|
||||
"""After modifying the content, replace the placeholders with the django template tags and variables."""
|
||||
for i, placeholder in enumerate(self.django_syntax_placeholders, 1):
|
||||
content = content.replace(
|
||||
f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{i}__", placeholder
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
def _revert_bs4_attribute_empty_attribute_fixing(self, contents):
|
||||
"""
|
||||
Removes empty attribute values added by BeautifulSoup to Django template tags.
|
||||
|
||||
BeautifulSoup adds ="" to empty attribute-like parts in HTML-like nodes.
|
||||
This method removes these additions for Django template tags.
|
||||
|
||||
Examples:
|
||||
- <div {{ something }}=""> becomes <div {{ something }}>
|
||||
- <div {% something %}=""> becomes <div {% something %}>
|
||||
"""
|
||||
# Remove ="" after Django variable tags
|
||||
contents = contents.replace('}}=""', "}}")
|
||||
|
||||
# Remove ="" after Django template tags
|
||||
contents = contents.replace('%}=""', "%}")
|
||||
|
||||
return contents
|
||||
|
||||
def _wrap_with_cotton_vars_frame(self, soup, cvars_el):
|
||||
"""Wrap content with {% cotton_vars_frame %} to be able to govern vars and attributes. In order to recognise
|
||||
vars defined in a component and also have them available in the same component's context, we wrap the entire
|
||||
contents in another component: cotton_vars_frame."""
|
||||
|
||||
vars_with_defaults = []
|
||||
for var, value in cvars_el.attrs.items():
|
||||
# Attributes in context at this point will already have been formatted in _component to be accessible, so in order to cascade match the style.
|
||||
accessible_var = var.replace("-", "_")
|
||||
|
||||
if value is None:
|
||||
vars_with_defaults.append(f"{var}={accessible_var}")
|
||||
elif var.startswith(":"):
|
||||
# If ':' is present, the user wants to parse a literal string as the default value,
|
||||
# i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1".
|
||||
var = var[1:] # Remove the ':' prefix
|
||||
accessible_var = accessible_var[1:] # Remove the ':' prefix
|
||||
vars_with_defaults.append(
|
||||
f'{var}={accessible_var}|eval_default:"{value}"'
|
||||
)
|
||||
else:
|
||||
# Assuming value is already a string that represents the default value
|
||||
vars_with_defaults.append(f'{var}={accessible_var}|default:"{value}"')
|
||||
|
||||
cvars_el.decompose()
|
||||
|
||||
# Construct the {% with %} opening tag
|
||||
opening = "{% cotton_vars_frame " + " ".join(vars_with_defaults) + " %}"
|
||||
closing = "{% endcotton_vars_frame %}"
|
||||
|
||||
# Convert the remaining soup back to a string and wrap it within {% with %} block
|
||||
wrapped_content = (
|
||||
opening
|
||||
+ str(soup.encode(formatter=UnsortedAttributes()).decode("utf-8")).strip()
|
||||
+ closing
|
||||
)
|
||||
|
||||
# Since we can't replace the soup object itself, we create new soup instead
|
||||
new_soup = BeautifulSoup(wrapped_content, "html.parser")
|
||||
|
||||
return new_soup
|
||||
|
||||
def _transform_components(self, soup, parent_key):
|
||||
"""Replace <c-[component path]> tags with the {% cotton_component %} template tag"""
|
||||
for tag in soup.find_all(re.compile("^c-"), recursive=True):
|
||||
if tag.name == "c-slot":
|
||||
self._transform_named_slot(tag, parent_key)
|
||||
|
||||
continue
|
||||
|
||||
component_key = tag.name[2:]
|
||||
component_path = component_key.replace(".", "/").replace("-", "_")
|
||||
opening_tag = f"{{% cotton_component {'{}/{}.html'.format(settings.COTTON_DIR if hasattr(settings, 'COTTON_DIR') else 'cotton', component_path)} {component_key} "
|
||||
|
||||
# Store attributes that contain template expressions, they are when we use '{{' or '{%' in the value of an attribute
|
||||
expression_attrs = []
|
||||
|
||||
# Build the attributes
|
||||
for key, value in tag.attrs.items():
|
||||
# BS4 stores class values as a list, so we need to join them back into a string
|
||||
if key == "class":
|
||||
value = " ".join(value)
|
||||
|
||||
# Django templates tags cannot have {{ or {% expressions in their attribute values
|
||||
# Neither can they have new lines, let's treat them both as "expression attrs"
|
||||
if self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX in value or "\n" in value:
|
||||
expression_attrs.append((key, value))
|
||||
continue
|
||||
|
||||
opening_tag += ' {}="{}"'.format(key, value)
|
||||
opening_tag += " %}"
|
||||
|
||||
component_tag = opening_tag
|
||||
|
||||
if expression_attrs:
|
||||
for key, value in expression_attrs:
|
||||
component_tag += f"{{% cotton_slot {key} {component_key} expression_attr %}}{value}{{% end_cotton_slot %}}"
|
||||
|
||||
if tag.contents:
|
||||
tag_soup = BeautifulSoup(tag.decode_contents(), "html.parser")
|
||||
self._transform_components(tag_soup, component_key)
|
||||
component_tag += str(
|
||||
tag_soup.encode(formatter=UnsortedAttributes()).decode("utf-8")
|
||||
)
|
||||
|
||||
component_tag += "{% end_cotton_component %}"
|
||||
|
||||
# Replace the original tag with the compiled django syntax
|
||||
new_soup = BeautifulSoup(component_tag, "html.parser")
|
||||
tag.replace_with(new_soup)
|
||||
|
||||
return soup
|
||||
|
||||
def _transform_named_slot(self, slot_tag, component_key):
|
||||
"""Compile <c-slot> to {% cotton_slot %}"""
|
||||
slot_name = slot_tag.get("name", "").strip()
|
||||
inner_html = "".join(str(content) for content in slot_tag.contents)
|
||||
|
||||
# Check and process any components in the slot content
|
||||
slot_soup = BeautifulSoup(inner_html, "html.parser")
|
||||
self._transform_components(slot_soup, component_key)
|
||||
|
||||
cotton_slot_tag = f"{{% cotton_slot {slot_name} {component_key} %}}{str(slot_soup.encode(formatter=UnsortedAttributes()).decode('utf-8'))}{{% end_cotton_slot %}}"
|
||||
|
||||
slot_tag.replace_with(BeautifulSoup(cotton_slot_tag, "html.parser"))
|
||||
|
||||
|
||||
class CottonTemplateCacheHandler:
|
||||
"""Handles caching of cotton templates so the html parsing is only done on first load of each view or component."""
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = getattr(settings, "COTTON_TEMPLATE_CACHING_ENABLED", True)
|
||||
|
||||
def get_cache_key(self, template_name, mtime):
|
||||
template_hash = hashlib.sha256(template_name.encode()).hexdigest()
|
||||
return f"cotton_cache_{template_hash}_{mtime}"
|
||||
|
||||
def get_cached_template(self, cache_key):
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
return cache.get(cache_key)
|
||||
|
||||
def cache_template(self, cache_key, content, timeout=None):
|
||||
if self.enabled:
|
||||
cache.set(cache_key, content, timeout=timeout)
|
|
@ -1,3 +0,0 @@
|
|||
<c-merges-attributes class="extra-class" silica:model="test" another="test">
|
||||
ss
|
||||
</c-merges-attributes>
|
|
@ -1,3 +0,0 @@
|
|||
<c-receives-attributes attribute_1="hello" and-another="woo1" thirdForLuck="yes">
|
||||
ss
|
||||
</c-receives-attributes>
|
|
@ -1,3 +0,0 @@
|
|||
<c-parent>
|
||||
<c-child>d</c-child>
|
||||
</c-parent>
|
|
@ -1 +0,0 @@
|
|||
<div class="i-am-child"></div>
|
|
@ -1,11 +0,0 @@
|
|||
<div>
|
||||
Header:
|
||||
{{ header }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Content:
|
||||
{{ slot }}
|
||||
</div>
|
||||
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{% if none is None %}
|
||||
<p>none is None</p>
|
||||
{% endif %}
|
||||
|
||||
{% if number == 1 %}
|
||||
<p>number is 1</p>
|
||||
{% endif %}
|
||||
|
||||
{% if boolean_true is True %}
|
||||
<p>boolean_true is True</p>
|
||||
{% endif %}
|
||||
|
||||
{% if boolean_false is False %}
|
||||
<p>boolean_false is False</p>
|
||||
{% endif %}
|
||||
|
||||
{% if dict.key == 'value' %}
|
||||
<p>dict.key is 'value'</p>
|
||||
{% endif %}
|
||||
|
||||
{% if list.0 == 1 %}
|
||||
<p>list.0 is 1</p>
|
||||
{% endif %}
|
||||
|
||||
{% if listdict.0.key == 'value' %}
|
||||
<p>listdict.0.key is 'value'</p>
|
||||
{% endif %}
|
|
@ -1,29 +0,0 @@
|
|||
<c-vars :none="None" :number="1" :boolean_true="True" :boolean_false="False" :dict="{'key': 'value'}" :list="[1, 2, 3]" :listdict="[{'key': 'value'}]" />
|
||||
|
||||
{% if none is None %}
|
||||
<p>none is None</p>
|
||||
{% endif %}
|
||||
|
||||
{% if number == 1 %}
|
||||
<p>number is 1</p>
|
||||
{% endif %}
|
||||
|
||||
{% if boolean_true is True %}
|
||||
<p>boolean_true is True</p>
|
||||
{% endif %}
|
||||
|
||||
{% if boolean_false is False %}
|
||||
<p>boolean_false is False</p>
|
||||
{% endif %}
|
||||
|
||||
{% if dict.key == 'value' %}
|
||||
<p>dict.key is 'value'</p>
|
||||
{% endif %}
|
||||
|
||||
{% if list.0 == 1 %}
|
||||
<p>list.0 is 1</p>
|
||||
{% endif %}
|
||||
|
||||
{% if listdict.0.key == 'value' %}
|
||||
<p>listdict.0.key is 'value'</p>
|
||||
{% endif %}
|
|
@ -1,3 +0,0 @@
|
|||
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}>
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
{{ name }}
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
Attribute 1 says: '{{ attr1 }}'
|
||||
Attribute 2 says: '{{ attr2 }}'
|
||||
Attribute 3 says: '{{ attr3 }}'
|
||||
attrs tag is: '{{ attrs }}'
|
|
@ -1,3 +0,0 @@
|
|||
<div class="i-am-parent">
|
||||
{{slot}}
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div {{ attrs }}>
|
||||
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
<c-vars var1 default_var="default var" />
|
||||
|
||||
<p>slot: '{{ slot }}'</p>
|
||||
|
||||
<p>attr1: '{{ attr1 }}'</p>
|
||||
<p>attr2: '{{ attr2 }}'</p>
|
||||
|
||||
<p>var1: '{{ var1 }}'</p>
|
||||
<p>default_var: '{{ default_var }}'</p>
|
||||
|
||||
<p>named_slot: '{{ named_slot }}'</p>
|
||||
|
||||
<p>attrs: '{{ attrs }}'</p>
|
|
@ -1,3 +0,0 @@
|
|||
{% if attr1 is True %}
|
||||
It's True
|
||||
{% endif %}
|
|
@ -1 +0,0 @@
|
|||
<div class="{% if 1 < 2 %} some-class {% endif %}">Hello, World!</div>
|
|
@ -1,9 +0,0 @@
|
|||
<c-eval-attributes-test-component
|
||||
:none="None"
|
||||
:number="1"
|
||||
:boolean_true="True"
|
||||
:boolean_false="False"
|
||||
:dict="{'key': 'value'}"
|
||||
:list="[1, 2, 3]"
|
||||
:listdict="[{'key': 'value'}]"
|
||||
/>
|
|
@ -1 +0,0 @@
|
|||
<c-eval-vars-test-component />
|
|
@ -1,3 +0,0 @@
|
|||
<c-parent>
|
||||
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
|
||||
</c-parent>
|
|
@ -1,7 +0,0 @@
|
|||
{% for item in items %}
|
||||
<c-named-slot-component>
|
||||
<c-slot name="name">
|
||||
item name: {{ item.name }}
|
||||
</c-slot>
|
||||
</c-named-slot-component>
|
||||
{% endfor %}
|
|
@ -1,12 +0,0 @@
|
|||
<c-vars test="world" name="Will"></c-vars>
|
||||
|
||||
<!-- create different types of native tag examples -->
|
||||
|
||||
<c-native-tags-in-attributes
|
||||
attr1="Hello {{ name }}"
|
||||
attr2="{{ test|default:"none" }}"
|
||||
attr3="{% if 1 == 1 %}cowabonga!{% endif %}"
|
||||
normal="normal"
|
||||
>
|
||||
<c-slot name="named">test</c-slot>
|
||||
</c-native-tags-in-attributes>
|
|
@ -1 +0,0 @@
|
|||
<c-parent></c-parent>
|
|
@ -1,4 +0,0 @@
|
|||
{% load static %}
|
||||
|
||||
|
||||
<c-parent/>
|
|
@ -1,5 +0,0 @@
|
|||
<c-test-component var1="string with space" attr1="I have spaces">
|
||||
<c-slot name="named_slot">
|
||||
named_slot with spaces
|
||||
</c-slot>
|
||||
</c-test-component>
|
|
@ -1 +0,0 @@
|
|||
<c-valueless-attribute-test-component attr1 />
|
|
@ -1,2 +0,0 @@
|
|||
<c-test-component attr1="variable" :attr2="variable">
|
||||
</c-test-component>
|
|
@ -1,115 +0,0 @@
|
|||
import ast
|
||||
from functools import lru_cache
|
||||
|
||||
from django import template
|
||||
from django.template import Node
|
||||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from django_cotton.utils import ensure_quoted
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_cached_template(template_name):
|
||||
return get_template(template_name)
|
||||
|
||||
|
||||
def render_template(template_name, context):
|
||||
template = get_cached_template(template_name)
|
||||
return template.render(context)
|
||||
|
||||
|
||||
def cotton_component(parser, token):
|
||||
"""
|
||||
Template tag to render a cotton component with dynamic attributes.
|
||||
|
||||
Usage:
|
||||
{% cotton_component 'template_path' 'component_key' key1="value1" :key2="dynamic_value" %}
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
template_path = bits[1]
|
||||
component_key = bits[2]
|
||||
|
||||
kwargs = {}
|
||||
for bit in bits[3:]:
|
||||
key, value = bit.split("=")
|
||||
kwargs[key] = value
|
||||
|
||||
nodelist = parser.parse(("end_cotton_component",))
|
||||
parser.delete_first_token()
|
||||
|
||||
return CottonComponentNode(nodelist, template_path, component_key, kwargs)
|
||||
|
||||
|
||||
class CottonComponentNode(Node):
|
||||
def __init__(self, nodelist, template_path, component_key, kwargs):
|
||||
self.nodelist = nodelist
|
||||
self.template_path = template_path
|
||||
self.component_key = component_key
|
||||
self.kwargs = kwargs
|
||||
|
||||
def render(self, context):
|
||||
local_context = context.flatten()
|
||||
attrs = {}
|
||||
|
||||
for key, value in self.kwargs.items():
|
||||
value = value.strip("'\"")
|
||||
|
||||
if key.startswith(":"):
|
||||
key = key[1:]
|
||||
attrs[key] = self.process_dynamic_attribute(value, context)
|
||||
elif value == "":
|
||||
attrs[key] = True
|
||||
else:
|
||||
attrs[key] = value
|
||||
|
||||
# Add the remainder as the default slot
|
||||
local_context["slot"] = self.nodelist.render(context)
|
||||
|
||||
# Merge slots and attributes into the local context
|
||||
all_slots = context.get("cotton_slots", {})
|
||||
component_slots = all_slots.get(self.component_key, {})
|
||||
local_context.update(component_slots)
|
||||
|
||||
# We need to check if any dynamic attributes are present in the component slots and move them over to attrs
|
||||
if "ctn_template_expression_attrs" in component_slots:
|
||||
for expression_attr in component_slots["ctn_template_expression_attrs"]:
|
||||
attrs[expression_attr] = component_slots[expression_attr]
|
||||
|
||||
# Build attrs string before formatting any '-' to '_' in attr names
|
||||
attrs_string = " ".join(
|
||||
f"{key}={ensure_quoted(value)}" for key, value in attrs.items()
|
||||
)
|
||||
local_context["attrs"] = mark_safe(attrs_string)
|
||||
|
||||
# Make the attrs available in the context for the vars frame, also before formatting the attr names
|
||||
local_context["attrs_dict"] = attrs
|
||||
|
||||
# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
|
||||
attrs = {key.replace("-", "_"): value for key, value in attrs.items()}
|
||||
local_context.update(attrs)
|
||||
|
||||
# Reset the component's slots in context to prevent bleeding into sibling components
|
||||
all_slots[self.component_key] = {}
|
||||
|
||||
return render_template(self.template_path, local_context)
|
||||
|
||||
def process_dynamic_attribute(self, value, context):
|
||||
"""
|
||||
Process a dynamic attribute, resolving template variables and evaluating literals.
|
||||
"""
|
||||
try:
|
||||
return template.Variable(value).resolve(context)
|
||||
except template.VariableDoesNotExist:
|
||||
pass
|
||||
|
||||
# Check for boolean attribute
|
||||
if value == "":
|
||||
return True
|
||||
|
||||
# It's not a template var or boolean attribute,
|
||||
# attempt to evaluate literal string or pass back raw value
|
||||
try:
|
||||
return ast.literal_eval(value)
|
||||
except (ValueError, SyntaxError):
|
||||
return value
|
|
@ -1,57 +0,0 @@
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
def cotton_slot(parser, token):
|
||||
"""
|
||||
Template tag to render a cotton slot.
|
||||
is_expression_attr: bool whether the attribute is dynamic or not.
|
||||
dynamic attributes are from the use of native tags {{ {% in the component attribute. We put them through as a named
|
||||
slot so they can be rendered and provided as a template variable
|
||||
"""
|
||||
try:
|
||||
tag_name, slot_name, component_key, *optional = token.split_contents()
|
||||
is_expression_attr = optional[0] if optional else None
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError("incomplete c-slot %r" % token.contents)
|
||||
|
||||
nodelist = parser.parse(("end_cotton_slot",))
|
||||
parser.delete_first_token()
|
||||
return CottonSlotNode(slot_name, nodelist, component_key, is_expression_attr)
|
||||
|
||||
|
||||
class CottonSlotNode(template.Node):
|
||||
def __init__(self, slot_name, nodelist, component_key, is_expression_attr):
|
||||
self.slot_name = slot_name
|
||||
self.nodelist = nodelist
|
||||
self.component_key = component_key
|
||||
self.is_expression_attr = is_expression_attr
|
||||
|
||||
def render(self, context):
|
||||
# Add the rendered content to the context.
|
||||
if "cotton_slots" not in context:
|
||||
context.update({"cotton_slots": {}})
|
||||
|
||||
output = self.nodelist.render(context)
|
||||
|
||||
# Store the slot data in a component-namespaced dictionary
|
||||
if self.component_key not in context["cotton_slots"]:
|
||||
context["cotton_slots"][self.component_key] = {}
|
||||
|
||||
context["cotton_slots"][self.component_key][self.slot_name] = mark_safe(output)
|
||||
|
||||
# If the slot is a dynamic attribute, we record it so it can be transferred to attrs in the component
|
||||
if self.is_expression_attr:
|
||||
if (
|
||||
"ctn_template_expression_attrs"
|
||||
not in context["cotton_slots"][self.component_key]
|
||||
):
|
||||
context["cotton_slots"][self.component_key][
|
||||
"ctn_template_expression_attrs"
|
||||
] = []
|
||||
|
||||
context["cotton_slots"][self.component_key][
|
||||
"ctn_template_expression_attrs"
|
||||
].append(self.slot_name)
|
||||
|
||||
return ""
|
|
@ -1,65 +0,0 @@
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from django_cotton.utils import ensure_quoted
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
def cotton_vars_frame(parser, token):
|
||||
"""The job of the vars frame is:
|
||||
1. to filter out attributes declared as vars inside {{ attrs }} string.
|
||||
2. to provide default values to attributes.
|
||||
Because we're effecting variables inside the same component, which is not possible usually, we we wrap
|
||||
the vars frame around the contents of the component so we can govern the attributes and vars that are available.
|
||||
"""
|
||||
bits = token.split_contents()[1:] # Skip the tag name
|
||||
|
||||
# We dont use token_kwargs because it doesn't allow for hyphens in key names, i.e. x-data=""
|
||||
tag_kwargs = {}
|
||||
for bit in bits:
|
||||
key, value = bit.split("=")
|
||||
tag_kwargs[key] = parser.compile_filter(value)
|
||||
|
||||
nodelist = parser.parse(("endcotton_vars_frame",))
|
||||
parser.delete_first_token()
|
||||
return CottonVarsFrameNode(nodelist, tag_kwargs)
|
||||
|
||||
|
||||
class CottonVarsFrameNode(template.Node):
|
||||
def __init__(self, nodelist, kwargs):
|
||||
self.nodelist = nodelist
|
||||
self.kwargs = kwargs
|
||||
|
||||
def render(self, context):
|
||||
# Assume 'attrs' are passed from the parent and are available in the context
|
||||
component_attrs = context.get("attrs_dict", {})
|
||||
|
||||
# Initialize vars based on the frame's kwargs and parent attrs
|
||||
vars = {}
|
||||
for key, value in self.kwargs.items():
|
||||
# Check if the var exists in component attrs; if so, use it, otherwise use the resolved default
|
||||
if key in component_attrs:
|
||||
vars[key] = component_attrs[key]
|
||||
else:
|
||||
# Attempt to resolve each kwarg value (which may include template variables)
|
||||
resolved_value = value.resolve(context)
|
||||
vars[key] = resolved_value
|
||||
|
||||
# Overwrite 'attrs' in the local context by excluding keys that are identified as vars
|
||||
attrs_without_vars = {k: v for k, v in component_attrs.items() if k not in vars}
|
||||
|
||||
# Provide all of the attrs as a string to pass to the component before any '-' to '_' replacing
|
||||
attrs = " ".join(
|
||||
f"{key}={ensure_quoted(value)}" for key, value in attrs_without_vars.items()
|
||||
)
|
||||
context["attrs"] = mark_safe(attrs)
|
||||
|
||||
context["attrs_dict"] = attrs_without_vars
|
||||
|
||||
# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
|
||||
formatted_vars = {key.replace("-", "_"): value for key, value in vars.items()}
|
||||
|
||||
context.update(formatted_vars)
|
||||
|
||||
return self.nodelist.render(context)
|
|
@ -1,30 +0,0 @@
|
|||
from django import template
|
||||
from django.utils.html import format_html_join
|
||||
|
||||
from django_cotton.templatetags._component import cotton_component
|
||||
from django_cotton.templatetags._slot import cotton_slot
|
||||
from django_cotton.templatetags._vars_frame import cotton_vars_frame
|
||||
from django_cotton.utils import eval_string
|
||||
|
||||
register = template.Library()
|
||||
register.tag("cotton_component", cotton_component)
|
||||
register.tag("cotton_slot", cotton_slot)
|
||||
register.tag("cotton_vars_frame", cotton_vars_frame)
|
||||
|
||||
|
||||
@register.filter
|
||||
def merge(attrs, args):
|
||||
# attrs is expected to be a dictionary of existing attributes
|
||||
# args is a string of additional attributes to merge, e.g., "class:extra-class"
|
||||
for arg in args.split(","):
|
||||
key, value = arg.split(":", 1)
|
||||
if key in attrs:
|
||||
attrs[key] = value + " " + attrs[key]
|
||||
else:
|
||||
attrs[key] = value
|
||||
return format_html_join(" ", '{0}="{1}"', attrs.items())
|
||||
|
||||
|
||||
@register.filter
|
||||
def eval_default(value, arg):
|
||||
return value or eval_string(arg)
|
|
@ -1,78 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.urls import path
|
||||
from django.test import override_settings
|
||||
from django.views.generic import TemplateView
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class DynamicURLModule:
|
||||
def __init__(self):
|
||||
self.urlpatterns = []
|
||||
|
||||
def __call__(self):
|
||||
return self.urlpatterns
|
||||
|
||||
|
||||
class CottonInlineTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Set tmp dir and register a url module for our tmp files
|
||||
cls.temp_dir = tempfile.mkdtemp()
|
||||
cls.url_module = DynamicURLModule()
|
||||
cls.url_module_name = f"dynamic_urls_{cls.__name__}"
|
||||
sys.modules[cls.url_module_name] = cls.url_module
|
||||
|
||||
# Register our temp directory as a TEMPLATES path
|
||||
cls.new_templates_setting = settings.TEMPLATES.copy()
|
||||
cls.new_templates_setting[0]["DIRS"] = [
|
||||
cls.temp_dir
|
||||
] + cls.new_templates_setting[0]["DIRS"]
|
||||
|
||||
# Apply the setting
|
||||
cls.templates_override = override_settings(TEMPLATES=cls.new_templates_setting)
|
||||
cls.templates_override.enable()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Remove temporary directory and clean up modules"""
|
||||
cls.templates_override.disable()
|
||||
shutil.rmtree(cls.temp_dir, ignore_errors=True)
|
||||
del sys.modules[cls.url_module_name]
|
||||
super().tearDownClass()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear cache between tests so that we can use the same file names for simplicity"""
|
||||
cache.clear()
|
||||
|
||||
def create_template(self, name, content):
|
||||
"""Create a template file in the temporary directory and return the path"""
|
||||
path = os.path.join(self.temp_dir, name)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
return path
|
||||
|
||||
def make_view(self, template_name):
|
||||
"""Make a view that renders the given template"""
|
||||
return TemplateView.as_view(template_name=template_name)
|
||||
|
||||
def register_url(self, url, view):
|
||||
"""Register a URL pattern and returns path"""
|
||||
url_pattern = path(url, view)
|
||||
self.url_module.urlpatterns.append(url_pattern)
|
||||
return url_pattern
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.url_module.urlpatterns = []
|
||||
|
||||
def get_url_conf(self):
|
||||
return self.url_module_name
|
|
@ -1,276 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from django_cotton.tests.inline_test_case import CottonInlineTestCase
|
||||
from django_cotton.tests.utils import get_compiled, get_rendered
|
||||
|
||||
|
||||
class InlineTestCase(CottonInlineTestCase):
|
||||
def test_component_is_rendered(self):
|
||||
self.create_template(
|
||||
"cotton/component.html",
|
||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"view.html",
|
||||
"""<c-component>Hello, World!</c-component>""",
|
||||
)
|
||||
|
||||
# Register Url
|
||||
self.register_url("view/", self.make_view("view.html"))
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-component">')
|
||||
self.assertContains(response, "Hello, World!")
|
||||
|
||||
def test_new_lines_in_attributes_are_preserved(self):
|
||||
self.create_template(
|
||||
"cotton/preserved.html",
|
||||
"""<div {{ attrs }}>{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"preserved_view.html",
|
||||
"""
|
||||
<c-preserved x-data="{
|
||||
attr1: 'im an attr',
|
||||
var1: 'im a var',
|
||||
method() {
|
||||
return 'im a method';
|
||||
}
|
||||
}" />
|
||||
""",
|
||||
)
|
||||
|
||||
# Register Url
|
||||
self.register_url("view/", self.make_view("preserved_view.html"))
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertTrue(
|
||||
"""{
|
||||
attr1: 'im an attr',
|
||||
var1: 'im a var',
|
||||
method() {
|
||||
return 'im a method';
|
||||
}
|
||||
}"""
|
||||
in response.content.decode()
|
||||
)
|
||||
|
||||
def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores(
|
||||
self,
|
||||
):
|
||||
self.create_template(
|
||||
"cotton/hyphens.html",
|
||||
"""
|
||||
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"hyphens_view.html",
|
||||
"""
|
||||
<c-hyphens x-data="{}" x-init="do_something()" />
|
||||
""",
|
||||
)
|
||||
|
||||
# Register Url
|
||||
self.register_url("view/", self.make_view("hyphens_view.html"))
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertContains(response, 'x-data="{}" x-init="do_something()"')
|
||||
|
||||
def test_attribute_names_on_cvars_containing_hyphens_are_converted_to_underscores(
|
||||
self,
|
||||
):
|
||||
self.create_template(
|
||||
"cotton/cvar_hyphens.html",
|
||||
"""
|
||||
<c-vars x-data="{}" x-init="do_something()" />
|
||||
|
||||
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
|
||||
""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"cvar_hyphens_view.html",
|
||||
"""
|
||||
<c-cvar-hyphens />
|
||||
""",
|
||||
)
|
||||
|
||||
# Register Url
|
||||
self.register_url("view/", self.make_view("cvar_hyphens_view.html"))
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.get_url_conf()):
|
||||
response = self.client.get("/view/")
|
||||
|
||||
self.assertContains(response, 'x-data="{}" x-init="do_something()"')
|
||||
|
||||
def test_cotton_directory_can_be_configured(self):
|
||||
custom_dir = "components"
|
||||
|
||||
self.create_template(
|
||||
f"{custom_dir}/component.html",
|
||||
"""<div class="i-am-component">{{ slot }}</div>""",
|
||||
)
|
||||
|
||||
self.create_template(
|
||||
"view.html",
|
||||
"""<c-component>Hello, World!</c-component>""",
|
||||
)
|
||||
|
||||
# Register Url
|
||||
self.register_url("view/", self.make_view("view.html"))
|
||||
|
||||
# Override URLconf
|
||||
with self.settings(ROOT_URLCONF=self.get_url_conf(), COTTON_DIR=custom_dir):
|
||||
response = self.client.get("/view/")
|
||||
self.assertContains(response, '<div class="i-am-component">')
|
||||
self.assertContains(response, "Hello, World!")
|
||||
|
||||
|
||||
class CottonTestCase(TestCase):
|
||||
def test_parent_component_is_rendered(self):
|
||||
response = self.client.get("/parent")
|
||||
self.assertContains(response, '<div class="i-am-parent">')
|
||||
|
||||
def test_child_is_rendered(self):
|
||||
response = self.client.get("/child")
|
||||
self.assertContains(response, '<div class="i-am-parent">')
|
||||
self.assertContains(response, '<div class="i-am-child">')
|
||||
|
||||
def test_self_closing_is_rendered(self):
|
||||
response = self.client.get("/self-closing")
|
||||
self.assertContains(response, '<div class="i-am-parent">')
|
||||
|
||||
def test_named_slots_correctly_display_in_loop(self):
|
||||
response = self.client.get("/named-slot-in-loop")
|
||||
self.assertContains(response, "item name: Item 1")
|
||||
self.assertContains(response, "item name: Item 2")
|
||||
self.assertContains(response, "item name: Item 3")
|
||||
|
||||
def test_attribute_passing(self):
|
||||
response = self.client.get("/attribute-passing")
|
||||
self.assertContains(
|
||||
response, '<div attribute_1="hello" and-another="woo1" thirdforluck="yes">'
|
||||
)
|
||||
|
||||
def test_attribute_merging(self):
|
||||
response = self.client.get("/attribute-merging")
|
||||
self.assertContains(
|
||||
response, 'class="form-group another-class-with:colon extra-class"'
|
||||
)
|
||||
|
||||
def test_django_syntax_decoding(self):
|
||||
response = self.client.get("/django-syntax-decoding")
|
||||
self.assertContains(response, "some-class")
|
||||
|
||||
def test_vars_are_converted_to_vars_frame_tags(self):
|
||||
compiled = get_compiled(
|
||||
"""
|
||||
<c-vars var1="string with space" />
|
||||
|
||||
content
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
compiled,
|
||||
"""{% cotton_vars_frame var1=var1|default:"string with space" %}content{% endcotton_vars_frame %}""",
|
||||
)
|
||||
|
||||
def test_attrs_do_not_contain_vars(self):
|
||||
response = self.client.get("/vars-test")
|
||||
self.assertContains(response, "attr1: 'im an attr'")
|
||||
self.assertContains(response, "var1: 'im a var'")
|
||||
self.assertContains(response, """attrs: 'attr1="im an attr"'""")
|
||||
|
||||
def test_strings_with_spaces_can_be_passed(self):
|
||||
response = self.client.get("/string-with-spaces")
|
||||
self.assertContains(response, "attr1: 'I have spaces'")
|
||||
self.assertContains(response, "var1: 'string with space'")
|
||||
self.assertContains(response, "default_var: 'default var'")
|
||||
self.assertContains(response, "named_slot: '")
|
||||
self.assertContains(response, "named_slot with spaces")
|
||||
self.assertContains(response, """attrs: 'attr1="I have spaces"'""")
|
||||
|
||||
def test_named_slots_dont_bleed_into_sibling_components(self):
|
||||
html = """
|
||||
<c-test-component>
|
||||
component1
|
||||
<c-slot name="named_slot">named slot 1</c-slot>
|
||||
</c-test-component>
|
||||
<c-test-component>
|
||||
component2
|
||||
</c-test-component>
|
||||
"""
|
||||
|
||||
rendered = get_rendered(html)
|
||||
|
||||
self.assertTrue("named_slot: 'named slot 1'" in rendered)
|
||||
self.assertTrue("named_slot: ''" in rendered)
|
||||
|
||||
def test_template_variables_are_not_parsed(self):
|
||||
html = """
|
||||
<c-test-component attr1="variable" :attr2="variable">
|
||||
<c-slot name="named_slot">
|
||||
<a href="#" silica:click.prevent="variable = 'lineage'">test</a>
|
||||
</c-slot>
|
||||
</c-test-component>
|
||||
"""
|
||||
|
||||
rendered = get_rendered(html, {"variable": 1})
|
||||
|
||||
self.assertTrue("attr1: 'variable'" in rendered)
|
||||
self.assertTrue("attr2: '1'" in rendered)
|
||||
|
||||
def test_valueless_attributes_are_process_as_true(self):
|
||||
response = self.client.get("/test/valueless-attributes")
|
||||
|
||||
self.assertContains(response, "It's True")
|
||||
|
||||
def test_component_attributes_can_converted_to_python_types(self):
|
||||
response = self.client.get("/test/eval-attributes")
|
||||
|
||||
self.assertContains(response, "none is None")
|
||||
self.assertContains(response, "number is 1")
|
||||
self.assertContains(response, "boolean_true is True")
|
||||
self.assertContains(response, "boolean_false is False")
|
||||
self.assertContains(response, "list.0 is 1")
|
||||
self.assertContains(response, "dict.key is 'value'")
|
||||
self.assertContains(response, "listdict.0.key is 'value'")
|
||||
|
||||
def test_cvars_can_be_converted_to_python_types(self):
|
||||
response = self.client.get("/test/eval-vars")
|
||||
|
||||
self.assertContains(response, "none is None")
|
||||
self.assertContains(response, "number is 1")
|
||||
self.assertContains(response, "boolean_true is True")
|
||||
self.assertContains(response, "boolean_false is False")
|
||||
self.assertContains(response, "list.0 is 1")
|
||||
self.assertContains(response, "dict.key is 'value'")
|
||||
self.assertContains(response, "listdict.0.key is 'value'")
|
||||
|
||||
def test_attributes_can_contain_django_native_tags(self):
|
||||
response = self.client.get("/test/native-tags-in-attributes")
|
||||
|
||||
self.assertContains(response, "Attribute 1 says: 'Hello Will'")
|
||||
self.assertContains(response, "Attribute 2 says: 'world'")
|
||||
self.assertContains(response, "Attribute 3 says: 'cowabonga!'")
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"""attrs tag is: 'normal="normal" attr1="Hello Will" attr2="world" attr3="cowabonga!"'""",
|
||||
)
|
||||
|
||||
# TODO: implement inline test asset creation, i.e. store_template("native-tags-in-attributes", """)
|
|
@ -1,18 +0,0 @@
|
|||
from django.template import Context, Template
|
||||
|
||||
from django_cotton.cotton_loader import Loader as CottonLoader
|
||||
|
||||
|
||||
def get_compiled(template_string):
|
||||
return CottonLoader(engine=None).template_processor.process(
|
||||
template_string, "test_key"
|
||||
)
|
||||
|
||||
|
||||
def get_rendered(template_string, context: dict = None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
compiled_string = get_compiled(template_string)
|
||||
|
||||
return Template(compiled_string).render(Context(context))
|
|
@ -1,52 +0,0 @@
|
|||
from django.views.generic import TemplateView
|
||||
from . import views
|
||||
from django.urls import path
|
||||
|
||||
app_name = "django_cotton"
|
||||
|
||||
|
||||
class NamedSlotInLoop(TemplateView):
|
||||
template_name = "named_slot_in_loop.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
"items": [
|
||||
{"name": "Item 1"},
|
||||
{"name": "Item 2"},
|
||||
{"name": "Item 3"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("parent", TemplateView.as_view(template_name="parent_test.html")),
|
||||
path("child", TemplateView.as_view(template_name="child_test.html")),
|
||||
path(
|
||||
"self-closing",
|
||||
TemplateView.as_view(template_name="self_closing_test.html"),
|
||||
),
|
||||
path("include", TemplateView.as_view(template_name="cotton_include.html")),
|
||||
path("playground", TemplateView.as_view(template_name="playground.html")),
|
||||
path("tag", TemplateView.as_view(template_name="tag.html")),
|
||||
path("named-slot-in-loop", NamedSlotInLoop.as_view()),
|
||||
path("test/compiled-cotton", views.compiled_cotton_test_view),
|
||||
path("test/cotton", views.cotton_test_view),
|
||||
path("test/native-extends", views.native_extends_test_view),
|
||||
path("test/native-include", views.native_include_test_view),
|
||||
path("test/valueless-attributes", views.valueless_attributes_test_view),
|
||||
path("attribute-merging", views.attribute_merging_test_view),
|
||||
path("attribute-passing", views.attribute_passing_test_view),
|
||||
path("django-syntax-decoding", views.django_syntax_decoding_test_view),
|
||||
path(
|
||||
"string-with-spaces",
|
||||
TemplateView.as_view(template_name="string_with_spaces.html"),
|
||||
),
|
||||
path("vars-test", TemplateView.as_view(template_name="vars_test.html")),
|
||||
path("variable-parsing", views.variable_parsing_test_view),
|
||||
path("test/eval-vars", views.eval_vars_test_view),
|
||||
path("test/eval-attributes", views.eval_attributes_test_view),
|
||||
path(
|
||||
"test/native-tags-in-attributes",
|
||||
TemplateView.as_view(template_name="native_tags_in_attributes_view.html"),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
import ast
|
||||
|
||||
|
||||
def eval_string(value):
|
||||
"""
|
||||
Evaluate a string representation of a constant, list, or dictionary to the actual Python object.
|
||||
"""
|
||||
try:
|
||||
return ast.literal_eval(value)
|
||||
except (ValueError, SyntaxError):
|
||||
return value
|
||||
|
||||
|
||||
def ensure_quoted(value):
|
||||
if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
|
||||
return value
|
||||
else:
|
||||
return f'"{value}"'
|
|
@ -1,50 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# benchmark tests
|
||||
|
||||
|
||||
def compiled_cotton_test_view(request):
|
||||
return render(request, "compiled_cotton_test.html")
|
||||
|
||||
|
||||
def cotton_test_view(request):
|
||||
return render(request, "cotton_test.html")
|
||||
|
||||
|
||||
def native_extends_test_view(request):
|
||||
return render(request, "native_extends_test.html")
|
||||
|
||||
|
||||
def native_include_test_view(request):
|
||||
return render(request, "native_include_test.html")
|
||||
|
||||
|
||||
# Django tests
|
||||
|
||||
|
||||
def attribute_merging_test_view(request):
|
||||
return render(request, "attribute_merging_test.html")
|
||||
|
||||
|
||||
def attribute_passing_test_view(request):
|
||||
return render(request, "attribute_passing_test.html")
|
||||
|
||||
|
||||
def django_syntax_decoding_test_view(request):
|
||||
return render(request, "django_syntax_decoding_test.html")
|
||||
|
||||
|
||||
def variable_parsing_test_view(request):
|
||||
return render(request, "variable_parsing_test.html", {"variable": "some-class"})
|
||||
|
||||
|
||||
def valueless_attributes_test_view(request):
|
||||
return render(request, "valueless_attributes_test_view.html")
|
||||
|
||||
|
||||
def eval_vars_test_view(request):
|
||||
return render(request, "eval_vars_test_view.html")
|
||||
|
||||
|
||||
def eval_attributes_test_view(request):
|
||||
return render(request, "eval_attributes_test_view.html")
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
WSGI config for django_cotton 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", "example_project.settings")
|
||||
|
||||
application = get_wsgi_application()
|
Loading…
Add table
Add a link
Reference in a new issue