django-cotton-bs5 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cotton_bs5/fixtures.py +217 -0
- cotton_bs5/templates/cotton/accordion/index.html +1 -1
- cotton_bs5/templates/cotton/accordion/item.html +9 -3
- cotton_bs5/templates/cotton/breadcrumbs/index.html +1 -1
- cotton_bs5/templates/cotton/breadcrumbs/item.html +9 -12
- cotton_bs5/templates/cotton/button/index.html +5 -12
- cotton_bs5/templates/cotton/cotton_bs5/document.html +8 -0
- cotton_bs5/templates/cotton/dropdown/item.html +6 -15
- cotton_bs5/templates/cotton/grid/col.html +2 -2
- cotton_bs5/templates/cotton/grid/index.html +3 -2
- cotton_bs5/templates/cotton/hstack.html +4 -0
- cotton_bs5/templates/cotton/list_group/index.html +5 -3
- cotton_bs5/templates/cotton/list_group/item.html +6 -7
- cotton_bs5/templates/cotton/nav/item.html +4 -0
- cotton_bs5/templates/cotton/vstack.html +4 -0
- cotton_bs5/templates/cotton_bs5/document_component.html +101 -0
- cotton_bs5/templatetags/__init__.py +0 -0
- cotton_bs5/templatetags/cotton_bs5.py +267 -0
- {django_cotton_bs5-0.5.0.dist-info → django_cotton_bs5-0.6.0.dist-info}/METADATA +91 -6
- {django_cotton_bs5-0.5.0.dist-info → django_cotton_bs5-0.6.0.dist-info}/RECORD +23 -14
- django_cotton_bs5-0.6.0.dist-info/entry_points.txt +3 -0
- {django_cotton_bs5-0.5.0.dist-info → django_cotton_bs5-0.6.0.dist-info}/LICENSE +0 -0
- {django_cotton_bs5-0.5.0.dist-info → django_cotton_bs5-0.6.0.dist-info}/WHEEL +0 -0
cotton_bs5/fixtures.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Pytest fixtures for testing Django Cotton BS5 components."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from django.template import Context, Template
|
|
6
|
+
from django.test import RequestFactory
|
|
7
|
+
from django_cotton import render_component
|
|
8
|
+
from django_cotton.compiler_regex import CottonCompiler
|
|
9
|
+
|
|
10
|
+
compiler = CottonCompiler()
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def cotton_render():
|
|
14
|
+
"""
|
|
15
|
+
Fixture that renders Django-Cotton components and returns raw HTML.
|
|
16
|
+
|
|
17
|
+
Automatically provides a request object to be DRY. Component variables
|
|
18
|
+
are passed as kwargs.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
def test_something(cotton_render):
|
|
22
|
+
html = cotton_render(
|
|
23
|
+
'cotton_bs5.alert',
|
|
24
|
+
message="Hello",
|
|
25
|
+
type="success"
|
|
26
|
+
)
|
|
27
|
+
assert 'alert-success' in html
|
|
28
|
+
"""
|
|
29
|
+
factory = RequestFactory()
|
|
30
|
+
|
|
31
|
+
def _render(component_name, context=None, **kwargs):
|
|
32
|
+
"""
|
|
33
|
+
Render a Cotton component with automatic request injection.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
component_name: Component name in dotted notation (e.g., "cotton_bs5.alert")
|
|
37
|
+
context: Optional context dict to pass as component attributes
|
|
38
|
+
**kwargs: Component attributes (alternative to context dict)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Rendered HTML string
|
|
42
|
+
"""
|
|
43
|
+
request = factory.get("/")
|
|
44
|
+
return render_component(request, component_name, context, **kwargs)
|
|
45
|
+
|
|
46
|
+
return _render
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def cotton_render_soup():
|
|
51
|
+
"""
|
|
52
|
+
Fixture that renders Django-Cotton components and returns BeautifulSoup parsed HTML.
|
|
53
|
+
|
|
54
|
+
Automatically provides a request object and parses the result for easy testing.
|
|
55
|
+
Component variables are passed as kwargs.
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
def test_something(cotton_render_soup):
|
|
59
|
+
soup = cotton_render_soup(
|
|
60
|
+
'cotton_bs5.alert',
|
|
61
|
+
message="Hello",
|
|
62
|
+
type="success"
|
|
63
|
+
)
|
|
64
|
+
assert soup.find('div')['class'] == ['alert', 'alert-success']
|
|
65
|
+
"""
|
|
66
|
+
factory = RequestFactory()
|
|
67
|
+
|
|
68
|
+
def _render(component_name, context=None, **kwargs):
|
|
69
|
+
"""
|
|
70
|
+
Render a Cotton component with automatic request injection and parse with BeautifulSoup.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
component_name: Component name in dotted notation (e.g., "cotton_bs5.alert")
|
|
74
|
+
context: Optional context dict to pass as component attributes
|
|
75
|
+
**kwargs: Component attributes (alternative to context dict)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
BeautifulSoup parsed HTML object
|
|
79
|
+
"""
|
|
80
|
+
request = factory.get("/")
|
|
81
|
+
html = render_component(request, component_name, context, **kwargs)
|
|
82
|
+
return BeautifulSoup(html, "html.parser")
|
|
83
|
+
|
|
84
|
+
return _render
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.fixture
|
|
88
|
+
def cotton_render_string():
|
|
89
|
+
"""
|
|
90
|
+
Fixture that compiles and renders Django template strings containing Cotton component syntax.
|
|
91
|
+
|
|
92
|
+
This fixture takes a raw template string with Cotton components (e.g., <c-button>),
|
|
93
|
+
compiles it through django-cotton's compiler, then renders it through Django's
|
|
94
|
+
Template system. Useful for testing inline component markup without creating
|
|
95
|
+
separate template files.
|
|
96
|
+
|
|
97
|
+
Usage:
|
|
98
|
+
def test_button_in_template(cotton_render_string):
|
|
99
|
+
html = cotton_render_string("<c-button variant='primary'>Click me</c-button>")
|
|
100
|
+
assert 'btn-primary' in html
|
|
101
|
+
|
|
102
|
+
def test_with_context(cotton_render_string):
|
|
103
|
+
html = cotton_render_string(
|
|
104
|
+
"<c-alert>{{ message }}</c-alert>",
|
|
105
|
+
context={'message': 'Hello World'}
|
|
106
|
+
)
|
|
107
|
+
assert 'Hello World' in html
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A callable function that accepts:
|
|
111
|
+
- template_string (str): Django template string with Cotton component syntax
|
|
112
|
+
- context (dict, optional): Template context variables
|
|
113
|
+
|
|
114
|
+
The function returns the rendered HTML as a string.
|
|
115
|
+
"""
|
|
116
|
+
factory = RequestFactory()
|
|
117
|
+
|
|
118
|
+
def _render(template_string, context=None):
|
|
119
|
+
"""
|
|
120
|
+
Compile and render a template string with Cotton components.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
template_string: The Django template string containing Cotton component syntax
|
|
124
|
+
context: Optional context dict for template variables
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Rendered HTML string
|
|
128
|
+
"""
|
|
129
|
+
if context is None:
|
|
130
|
+
context = {}
|
|
131
|
+
request = factory.get("/")
|
|
132
|
+
context['request'] = request
|
|
133
|
+
|
|
134
|
+
# Compile Cotton component syntax into Django template syntax
|
|
135
|
+
compiled_template = compiler.process(template_string)
|
|
136
|
+
|
|
137
|
+
# Render through Django's Template system
|
|
138
|
+
django_template = Template(compiled_template)
|
|
139
|
+
django_context = Context(context)
|
|
140
|
+
return django_template.render(django_context)
|
|
141
|
+
|
|
142
|
+
return _render
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.fixture
|
|
146
|
+
def cotton_render_string_soup():
|
|
147
|
+
"""
|
|
148
|
+
Fixture that compiles and renders Django template strings with Cotton components,
|
|
149
|
+
returning BeautifulSoup parsed HTML.
|
|
150
|
+
|
|
151
|
+
This fixture combines the capabilities of cotton_render_string with BeautifulSoup
|
|
152
|
+
parsing for easier DOM traversal and assertions. Particularly useful for testing
|
|
153
|
+
multi-component features where you need to verify nested structure and relationships.
|
|
154
|
+
|
|
155
|
+
Usage:
|
|
156
|
+
def test_nested_list(cotton_render_string_soup):
|
|
157
|
+
soup = cotton_render_string_soup(
|
|
158
|
+
"<c-ul><c-li text='first' /><c-li text='second' /></c-ul>"
|
|
159
|
+
)
|
|
160
|
+
items = soup.find_all('li')
|
|
161
|
+
assert len(items) == 2
|
|
162
|
+
assert items[0].get_text() == 'first'
|
|
163
|
+
assert items[1].get_text() == 'second'
|
|
164
|
+
|
|
165
|
+
def test_complex_layout_with_context(cotton_render_string_soup):
|
|
166
|
+
template = '''
|
|
167
|
+
<c-card>
|
|
168
|
+
<c-card.title>{{ title }}</c-card.title>
|
|
169
|
+
<c-card.body>
|
|
170
|
+
<c-button variant='primary'>{{ action }}</c-button>
|
|
171
|
+
</c-card.body>
|
|
172
|
+
</c-card>
|
|
173
|
+
'''
|
|
174
|
+
soup = cotton_render_string_soup(template, context={
|
|
175
|
+
'title': 'My Card',
|
|
176
|
+
'action': 'Click Here'
|
|
177
|
+
})
|
|
178
|
+
assert soup.find('h5').get_text() == 'My Card'
|
|
179
|
+
assert 'btn-primary' in soup.find('button')['class']
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A callable function that accepts:
|
|
183
|
+
- template_string (str): Django template string with Cotton component syntax
|
|
184
|
+
- context (dict, optional): Template context variables
|
|
185
|
+
|
|
186
|
+
The function returns a BeautifulSoup parsed HTML object.
|
|
187
|
+
"""
|
|
188
|
+
factory = RequestFactory()
|
|
189
|
+
|
|
190
|
+
def _render(template_string, context=None):
|
|
191
|
+
"""
|
|
192
|
+
Compile, render, and parse a template string with Cotton components.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
template_string: The Django template string containing Cotton component syntax
|
|
196
|
+
context: Optional context dict for template variables
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
BeautifulSoup parsed HTML object
|
|
200
|
+
"""
|
|
201
|
+
if context is None:
|
|
202
|
+
context = {}
|
|
203
|
+
request = factory.get("/")
|
|
204
|
+
context['request'] = request
|
|
205
|
+
|
|
206
|
+
# Compile Cotton component syntax into Django template syntax
|
|
207
|
+
compiled_template = compiler.process(template_string)
|
|
208
|
+
|
|
209
|
+
# Render through Django's Template system
|
|
210
|
+
django_template = Template(compiled_template)
|
|
211
|
+
django_context = Context(context)
|
|
212
|
+
html = django_template.render(django_context)
|
|
213
|
+
|
|
214
|
+
# Parse with BeautifulSoup for easy DOM traversal
|
|
215
|
+
return BeautifulSoup(html, "html.parser")
|
|
216
|
+
|
|
217
|
+
return _render
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
{% load cotton_bs5 %}
|
|
2
|
+
<c-vars id text target header class show />
|
|
3
|
+
{% cotton_parent as parent %}
|
|
4
|
+
{% genid "accordion-item" as uid %}
|
|
5
|
+
{% firstof id uid as target %}
|
|
2
6
|
<div class="accordion-item {{ class }}" {{ attrs }}>
|
|
3
|
-
{% if text %}<c-accordion.header :attrs="attrs" />{% endif %}
|
|
7
|
+
{% if text %}<c-accordion.header target="{{ target }}" :attrs="attrs" />{% endif %}
|
|
4
8
|
{{ header }}
|
|
5
|
-
<c-accordion.body id="{{ target }}"
|
|
9
|
+
<c-accordion.body id="{{ target }}"
|
|
10
|
+
parent="{{ parent.id }}"
|
|
11
|
+
:attrs="attrs">
|
|
6
12
|
{{ slot }}
|
|
7
13
|
</c-accordion.body>
|
|
8
14
|
</div>
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
<c-vars
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{{ text }}{{ slot }}
|
|
9
|
-
</a>
|
|
10
|
-
{% else %}
|
|
11
|
-
{{ text }}{{ slot }}
|
|
12
|
-
{% endif %}
|
|
1
|
+
<c-vars text class />
|
|
2
|
+
{% with element=href|yesno:"a,span" %}
|
|
3
|
+
{# djlint:off #}
|
|
4
|
+
<li class="breadcrumb-item{% if element == 'span' %} active{% endif %} {{ class }}"{% if element == 'span' %} aria-current="page"{% endif %}>
|
|
5
|
+
<{{ element }}{% if href %} class="link-underline link-underline-opacity-0 link-underline-opacity-75-hover"{% endif %} {{ attrs }}>
|
|
6
|
+
{{ text }}{{ slot }}
|
|
7
|
+
</{{ element }}>
|
|
13
8
|
</li>
|
|
9
|
+
{# djlint:on #}
|
|
10
|
+
{% endwith %}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
<c-vars variant="primary" size="" outline="" text class />
|
|
2
2
|
{# djlint:off #}
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
{
|
|
8
|
-
{% if slot %}
|
|
9
|
-
{{ slot }}
|
|
10
|
-
{% else %}
|
|
11
|
-
{{ text }}
|
|
12
|
-
{% endif %}
|
|
13
|
-
{# djlint:off #}
|
|
14
|
-
</{% if href %}a{% else %}button{% endif %}>
|
|
3
|
+
{% with element=href|yesno:"a,button" %}
|
|
4
|
+
<{{ element }} class="btn{% if outline %} btn-outline-{{ variant }}{% else %} btn-{{ variant }}{% endif %}{% if size %} btn-{{ size }}{% endif %} {{ class }}" {{ attrs }}>
|
|
5
|
+
{{ slot }} {{ text }}
|
|
6
|
+
</{{ element }}>
|
|
7
|
+
{% endwith %}
|
|
15
8
|
{# djlint:on #}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
<c-vars
|
|
2
|
-
{%
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</a>
|
|
8
|
-
{% else %}
|
|
9
|
-
<button class="dropdown-item{% if active %} active{% endif %}{% if disabled %} disabled{% endif %} {{ class }}"
|
|
10
|
-
type="button"
|
|
11
|
-
{% if disabled %}disabled{% endif %}
|
|
12
|
-
{{ attrs }}>
|
|
13
|
-
{{ text }} {{ slot }}
|
|
14
|
-
</button>
|
|
15
|
-
{% endif %}
|
|
1
|
+
<c-vars text active disabled class />
|
|
2
|
+
{% with element=href|yesno:"a,button" %}
|
|
3
|
+
{# djlint:off #}
|
|
4
|
+
<{{ element }} class="dropdown-item{% if active %} active{% endif %}{% if disabled %} disabled{% endif %}{% if class %} {{ class }}{% endif %}"{% if element == "button" %} type="button"{% endif %}{% if disabled %}{% if element == "a" %} tabindex="-1" aria-disabled="true"{% else %} disabled{% endif %}{% endif %} {{ attrs }}>{{ text }}{{ slot }}</{{ element }}>
|
|
5
|
+
{# djlint:on #}
|
|
6
|
+
{% endwith %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<c-vars width="" :responsive="{}" offset="" class />
|
|
2
|
-
<div class="{% if width %}col-{{ width }}
|
|
1
|
+
<c-vars width="" :responsive="{}" offset="" gap=0 class />
|
|
2
|
+
<div class="{% if width %}col-{{ width }}{% else %}col{% endif %}{% for bp, width in responsive.items %} col-{{ bp }}-{{ width }}{% endfor %} gap-{{ gap }} {{ class }}"
|
|
3
3
|
{{ attrs }}>
|
|
4
4
|
{{ slot }}
|
|
5
5
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<c-vars cols=
|
|
2
|
-
<div class="row{% if cols %} row-cols-{{ cols }}{% endif %}{% if gap %} g-{{ gap }}{% endif %}{%
|
|
1
|
+
<c-vars cols=1 gap="1" sm md lg xl xxl class />
|
|
2
|
+
<div class="row{% if cols %} row-cols-{{ cols }}{% endif %}{% if gap %} g-{{ gap }}{% endif %}{% if sm %} row-cols-sm-{{ sm }}{% endif %}{% if md %} row-cols-md-{{ sm }}{% endif %}{% if lg %} row-cols-lg-{{ lg }}{% endif %}{% if xl %} row-cols-xl-{{ xl }}{% endif %}{% if xl %} row-cols-xl-{{ xl }}{% endif %}{% if xxl %} row-cols-xxl-{{ xxl }}{% endif %}{{ class }}"
|
|
3
3
|
{{ attrs }}>
|
|
4
4
|
{{ slot }}
|
|
5
5
|
</div>
|
|
6
|
+
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
:horizontal="False"
|
|
4
4
|
class />
|
|
5
5
|
{# djlint:off #}
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
{% with numbered|yesno:"ol,ul" as element %}
|
|
7
|
+
<{{ element }}
|
|
8
|
+
class="list-group{% if flush %} list-group-flush{% endif %}{% if horizontal %} list-group-horizontal{% if horizontal != True %}-{{ horizontal }}{% endif %}{% endif %} {{ class }}"
|
|
8
9
|
{{ attrs }}>
|
|
9
10
|
{{ slot }}
|
|
10
|
-
</{
|
|
11
|
+
</{{ element }}>
|
|
12
|
+
{% endwith %}
|
|
11
13
|
{# djlint:on #}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
<c-vars text active disabled variant
|
|
1
|
+
<c-vars text active disabled variant class />
|
|
2
2
|
{# djlint:off #}
|
|
3
|
-
|
|
4
|
-
class="list-group-item{% if href %} list-group-item-action{% endif %}{% if active %} active{% elif disabled %} disabled{% endif %}{% if variant %} list-group-item-{{ variant }}{% endif %} {{ class }}"
|
|
5
|
-
{
|
|
6
|
-
{{
|
|
7
|
-
{
|
|
8
|
-
</{% if href %}a{% else %}{{ tag }}{% endif %}>
|
|
3
|
+
{% with element=href|yesno:"a,li" %}
|
|
4
|
+
<{{ element }} class="list-group-item{% if href %} list-group-item-action{% endif %}{% if active %} active{% elif disabled %} disabled{% endif %}{% if variant %} list-group-item-{{ variant }}{% endif %} {{ class }}"{% if active %} aria-current="true"{% elif disabled %} aria-disabled="true"{% endif %} {{ attrs }}>
|
|
5
|
+
{{ text }}{{ slot }}
|
|
6
|
+
</{{ element }}>
|
|
7
|
+
{% endwith %}
|
|
9
8
|
{# djlint:on #}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{% load cotton_bs5 %}
|
|
2
|
+
<div class="card component-demo-card">
|
|
3
|
+
<!-- Rendered Component Preview -->
|
|
4
|
+
<div class="demo-preview border-bottom p-4">{{ rendered }}</div>
|
|
5
|
+
<!-- Code Tabs Navigation -->
|
|
6
|
+
<div class="border-bottom">
|
|
7
|
+
<div role="tablist"
|
|
8
|
+
class="nav nav-tabs border-0 px-3 pt-2">
|
|
9
|
+
{% genid "syntax-tab" as syntax_tab_id %}
|
|
10
|
+
<button type="button"
|
|
11
|
+
class="nav-link active"
|
|
12
|
+
id="{{ syntax_tab_id }}"
|
|
13
|
+
data-bs-toggle="tab"
|
|
14
|
+
data-bs-target="#{{ syntax_tab_id }}-pane"
|
|
15
|
+
role="tab"
|
|
16
|
+
aria-controls="{{ syntax_tab_id }}-pane"
|
|
17
|
+
aria-selected="true">
|
|
18
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
19
|
+
width="14"
|
|
20
|
+
height="14"
|
|
21
|
+
fill="currentColor"
|
|
22
|
+
class="bi bi-code-slash me-1"
|
|
23
|
+
viewBox="0 0 16 16">
|
|
24
|
+
<path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294l4-13zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0zm6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z" />
|
|
25
|
+
</svg>
|
|
26
|
+
Cotton Syntax
|
|
27
|
+
</button>
|
|
28
|
+
{% genid "html-tab" as html_tab_id %}
|
|
29
|
+
<button type="button"
|
|
30
|
+
class="nav-link"
|
|
31
|
+
id="{{ html_tab_id }}"
|
|
32
|
+
data-bs-toggle="tab"
|
|
33
|
+
data-bs-target="#{{ html_tab_id }}-pane"
|
|
34
|
+
role="tab"
|
|
35
|
+
aria-controls="{{ html_tab_id }}-pane"
|
|
36
|
+
aria-selected="false">
|
|
37
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
38
|
+
width="14"
|
|
39
|
+
height="14"
|
|
40
|
+
fill="currentColor"
|
|
41
|
+
class="bi bi-file-code me-1"
|
|
42
|
+
viewBox="0 0 16 16">
|
|
43
|
+
<path d="M6.646 5.646a.5.5 0 1 1 .708.708L5.707 8l1.647 1.646a.5.5 0 0 1-.708.708l-2-2a.5.5 0 0 1 0-.708l2-2zm2.708 0a.5.5 0 1 0-.708.708L10.293 8 8.646 9.646a.5.5 0 0 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2z" />
|
|
44
|
+
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z" />
|
|
45
|
+
</svg>
|
|
46
|
+
HTML Output
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<!-- Code Tabs Content -->
|
|
51
|
+
<div class="tab-content">
|
|
52
|
+
<div class="tab-pane fade show active"
|
|
53
|
+
id="{{ syntax_tab_id }}-pane"
|
|
54
|
+
role="tabpanel"
|
|
55
|
+
aria-labelledby="{{ syntax_tab_id }}"
|
|
56
|
+
tabindex="0">
|
|
57
|
+
<div class="position-relative">
|
|
58
|
+
<button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 m-3 copy-btn"
|
|
59
|
+
data-copy-target="{{ syntax_tab_id }}-code"
|
|
60
|
+
aria-label="Copy Cotton syntax to clipboard"
|
|
61
|
+
title="Copy code">
|
|
62
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
63
|
+
width="14"
|
|
64
|
+
height="14"
|
|
65
|
+
fill="currentColor"
|
|
66
|
+
class="bi bi-clipboard"
|
|
67
|
+
viewBox="0 0 16 16">
|
|
68
|
+
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
|
|
69
|
+
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
|
|
70
|
+
</svg>
|
|
71
|
+
Copy
|
|
72
|
+
</button>
|
|
73
|
+
<pre class="mb-0 p-3"><code id="{{ syntax_tab_id }}-code" class="language-html">{{ code }}</code></pre>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="tab-pane fade"
|
|
77
|
+
id="{{ html_tab_id }}-pane"
|
|
78
|
+
role="tabpanel"
|
|
79
|
+
aria-labelledby="{{ html_tab_id }}"
|
|
80
|
+
tabindex="0">
|
|
81
|
+
<div class="position-relative">
|
|
82
|
+
<button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 m-3 copy-btn"
|
|
83
|
+
data-copy-target="{{ html_tab_id }}-code"
|
|
84
|
+
aria-label="Copy HTML output to clipboard"
|
|
85
|
+
title="Copy code">
|
|
86
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
87
|
+
width="14"
|
|
88
|
+
height="14"
|
|
89
|
+
fill="currentColor"
|
|
90
|
+
class="bi bi-clipboard"
|
|
91
|
+
viewBox="0 0 16 16">
|
|
92
|
+
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
|
|
93
|
+
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
|
|
94
|
+
</svg>
|
|
95
|
+
Copy
|
|
96
|
+
</button>
|
|
97
|
+
<pre class="mb-0 p-3"><code id="{{ html_tab_id }}-code" class="language-html">{{ html }}</code></pre>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Template tags and filters for MVP navbar widgets."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
from django import template
|
|
8
|
+
from django.template.loader import render_to_string
|
|
9
|
+
from django.utils.html import escape
|
|
10
|
+
from django.utils.safestring import mark_safe
|
|
11
|
+
from django_cotton.compiler_regex import CottonCompiler
|
|
12
|
+
|
|
13
|
+
# Conditional import for BeautifulSoup
|
|
14
|
+
try:
|
|
15
|
+
from bs4 import BeautifulSoup
|
|
16
|
+
|
|
17
|
+
HAS_BEAUTIFULSOUP = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
HAS_BEAUTIFULSOUP = False
|
|
20
|
+
|
|
21
|
+
register = template.Library()
|
|
22
|
+
|
|
23
|
+
compiler = CottonCompiler()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register.filter
|
|
27
|
+
def slot_is_empty(slot):
|
|
28
|
+
"""Check if a template slot is empty after stripping whitespace.
|
|
29
|
+
|
|
30
|
+
Django Cotton slots may contain strings with unwanted whitespace or newlines,
|
|
31
|
+
making direct template checks unreliable. This filter properly handles these
|
|
32
|
+
cases by stripping whitespace before comparison.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
slot: The slot content to check (typically a string, but may be other types).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
bool: True if slot is a string with only whitespace or is empty, False otherwise.
|
|
39
|
+
Returns None implicitly if slot is not a string type.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
{% if slot_content|slot_is_empty %}
|
|
43
|
+
<p>No content provided</p>
|
|
44
|
+
{% else %}
|
|
45
|
+
{{ slot_content }}
|
|
46
|
+
{% endif %}
|
|
47
|
+
"""
|
|
48
|
+
if isinstance(slot, str):
|
|
49
|
+
return slot.strip() == ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@register.filter
|
|
53
|
+
def beautify_html(html):
|
|
54
|
+
"""Clean up and format unformatted HTML with proper indentation.
|
|
55
|
+
|
|
56
|
+
Removes common leading whitespace and strips leading/trailing blank lines
|
|
57
|
+
to produce clean, readable HTML.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
html (str): Unformatted HTML string
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: Formatted HTML with normalized indentation
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
Input: " <div>\n <p>Hello</p>\n </div>"
|
|
67
|
+
Output: "<div>\n <p>Hello</p>\n</div>"
|
|
68
|
+
"""
|
|
69
|
+
return textwrap.dedent(html).strip("\n")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@register.simple_tag
|
|
73
|
+
def genid(prefix="", length=6):
|
|
74
|
+
"""Generate a unique random ID for use in HTML attributes.
|
|
75
|
+
|
|
76
|
+
Produces a short random string suitable for use as unique HTML element IDs.
|
|
77
|
+
Can optionally include a prefix for semantic naming.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
prefix (str): Optional prefix to prepend to the random string.
|
|
81
|
+
If provided, the format is "{prefix}-{random_string}".
|
|
82
|
+
Defaults to empty string (no prefix).
|
|
83
|
+
length (int): Length of the random string to generate.
|
|
84
|
+
Defaults to 6 characters.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
str: Generated ID. If prefix is provided, returns "{prefix}-{random_string}",
|
|
88
|
+
otherwise returns just the random string.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
{% genid %} # Returns something like "a3b2c1"
|
|
92
|
+
{% genid "tab" %} # Returns something like "tab-a3b2c1"
|
|
93
|
+
{% genid "modal" 8 %} # Returns something like "modal-a3b2c1d9"
|
|
94
|
+
{% genid prefix="button" length=10 %} # Returns something like "button-a3b2c1d9e7"
|
|
95
|
+
"""
|
|
96
|
+
random_str = "".join(
|
|
97
|
+
secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)
|
|
98
|
+
)
|
|
99
|
+
if prefix:
|
|
100
|
+
return f"{prefix}-{random_str}"
|
|
101
|
+
return random_str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@register.simple_tag(takes_context=True)
|
|
105
|
+
def cotton_parent(context):
|
|
106
|
+
cotton_data = context.get("cotton_data", {})
|
|
107
|
+
if not cotton_data:
|
|
108
|
+
return None
|
|
109
|
+
stack = cotton_data.get("stack", [])
|
|
110
|
+
if not stack:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
stack_length = len(stack)
|
|
114
|
+
|
|
115
|
+
parent_idx = stack_length - 2 # Get the index of the parent element
|
|
116
|
+
|
|
117
|
+
# this will ONLY return attrs declared on the component itself, not c-vars
|
|
118
|
+
return stack[parent_idx]["attrs"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@register.simple_tag(takes_context=True)
|
|
122
|
+
def responsive(context, root: str):
|
|
123
|
+
"""Generate responsive Bootstrap grid classes from context variables.
|
|
124
|
+
|
|
125
|
+
This tag generates responsive variants of a root class name (e.g., 'col')
|
|
126
|
+
by combining it with responsive breakpoint suffixes (xs, sm, md, lg, xl, xxl).
|
|
127
|
+
Context variables for each breakpoint determine whether the suffix is included
|
|
128
|
+
and its value.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
context (dict): Django template context, expected to contain optional keys:
|
|
132
|
+
- xs (str): Extra small breakpoint value
|
|
133
|
+
- sm (str): Small breakpoint value
|
|
134
|
+
- md (str): Medium breakpoint value
|
|
135
|
+
- lg (str): Large breakpoint value
|
|
136
|
+
- xl (str): Extra large breakpoint value
|
|
137
|
+
- xxl (str): Extra extra large breakpoint value
|
|
138
|
+
root (str): Base class name to which responsive variants are appended
|
|
139
|
+
(e.g., 'col', 'offset', 'gutter')
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
str: Space-separated responsive class names. For each breakpoint variable
|
|
143
|
+
present in context, returns "{root}-{breakpoint}-{value}".
|
|
144
|
+
Returns empty string if no breakpoint variables are defined.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
Context: {'md': '6', 'lg': '4', 'xl': '3'}
|
|
148
|
+
Tag: {% responsive 'col' %}
|
|
149
|
+
Output: 'col-md-6 col-lg-4 col-xl-3'
|
|
150
|
+
"""
|
|
151
|
+
# The idea is to take a root class name (e.g., "col") and
|
|
152
|
+
# and generate responsive variants based on context variables xs, sm, md, lg, xl, xxl).
|
|
153
|
+
# If a context variable is present, the value should be added to root along with the responsive
|
|
154
|
+
# name (e.g., "col-md-6").
|
|
155
|
+
|
|
156
|
+
responsive_values = {
|
|
157
|
+
responsive: context.get(responsive)
|
|
158
|
+
for responsive in ["xs", "sm", "md", "lg", "xl", "xxl"]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return " ".join(
|
|
162
|
+
f"{root}-{key}-{value}"
|
|
163
|
+
for key, value in responsive_values.items()
|
|
164
|
+
if value is not None
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@register.tag(name="show_code")
|
|
169
|
+
def show_code(parser, token):
|
|
170
|
+
"""Parse the show_code block tag.
|
|
171
|
+
|
|
172
|
+
Collects the template content between {% show_code %} and {% endshow_code %} tags
|
|
173
|
+
for processing and rendering.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
parser: Django template parser
|
|
177
|
+
token: Template token with tag name
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
ShowCodeNode: Node instance that will handle rendering
|
|
181
|
+
"""
|
|
182
|
+
nodelist = parser.parse(("endshow_code",))
|
|
183
|
+
parser.delete_first_token()
|
|
184
|
+
return ShowCodeNode(nodelist)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ShowCodeNode(template.Node):
|
|
188
|
+
"""Template node for the show_code tag.
|
|
189
|
+
|
|
190
|
+
Processes template content to display both executable code and its rendered output.
|
|
191
|
+
Handles indentation normalization, HTML escaping, Cotton template compilation,
|
|
192
|
+
and HTML rendering.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
nodelist (NodeList): Template nodes between show_code block start and end tags
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, nodelist):
|
|
199
|
+
"""Initialize the node with template content.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
nodelist: Template node list from parser between block tags
|
|
203
|
+
"""
|
|
204
|
+
self.nodelist = nodelist
|
|
205
|
+
|
|
206
|
+
def render(self, context):
|
|
207
|
+
"""Render the code block with normalized formatting and display.
|
|
208
|
+
|
|
209
|
+
Processes the captured template content through the following steps:
|
|
210
|
+
1. Render the template content to raw text
|
|
211
|
+
2. Normalize indentation (remove common leading whitespace)
|
|
212
|
+
3. Remove leading/trailing blank lines
|
|
213
|
+
4. Escape HTML special characters for safe display
|
|
214
|
+
5. Compile Cotton template syntax
|
|
215
|
+
6. Execute the compiled template
|
|
216
|
+
7. Display both formatted code and rendered output side-by-side
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
context: Django template context for rendering
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
str: HTML markup displaying the code and its rendered result
|
|
223
|
+
"""
|
|
224
|
+
raw = self.nodelist.render(context)
|
|
225
|
+
|
|
226
|
+
# Beautifulsoup does annoying things to the Cotton syntax, so we'll skip that step and clean ourselves
|
|
227
|
+
# soup = BeautifulSoup(raw, "html.parser")
|
|
228
|
+
# cleaned = soup.prettify(formatter="html5")
|
|
229
|
+
|
|
230
|
+
cleaned = textwrap.dedent(raw).strip("\n")
|
|
231
|
+
|
|
232
|
+
# 3. Escape Cotton syntax for HTML display in code tag
|
|
233
|
+
code = escape(cleaned)
|
|
234
|
+
|
|
235
|
+
# 4. Compile Cotton syntax
|
|
236
|
+
compiled = compiler.process(cleaned)
|
|
237
|
+
|
|
238
|
+
# 5. Render the compiled template
|
|
239
|
+
t = template.Template(compiled)
|
|
240
|
+
rendered_raw = t.render(context)
|
|
241
|
+
|
|
242
|
+
# 6. Clean the rendered HTML using BeautifulSoup for proper formatting
|
|
243
|
+
if not HAS_BEAUTIFULSOUP:
|
|
244
|
+
raise ImportError(
|
|
245
|
+
"BeautifulSoup4 is required for the show_code template tag. "
|
|
246
|
+
"Install it with: pip install beautifulsoup4"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
soup = BeautifulSoup(rendered_raw, "html.parser")
|
|
250
|
+
rendered_cleaned = soup.prettify()
|
|
251
|
+
|
|
252
|
+
# rendered_cleaned = textwrap.dedent(rendered_raw).strip("\n")
|
|
253
|
+
# rendered_cleaned = re.sub(r'\n\s*\n(\s*\n)+', '\n\n', rendered_cleaned)
|
|
254
|
+
|
|
255
|
+
# Remove excessive blank lines (more than one consecutive blank line)
|
|
256
|
+
# rendered_cleaned = rendered_cleaned.strip()
|
|
257
|
+
|
|
258
|
+
# 7. Mark safe for actual rendering on page
|
|
259
|
+
rendered = mark_safe(rendered_cleaned)
|
|
260
|
+
|
|
261
|
+
# 8. Escape cleaned HTML for display in code tag
|
|
262
|
+
html = escape(rendered_cleaned)
|
|
263
|
+
|
|
264
|
+
return render_to_string(
|
|
265
|
+
"cotton_bs5/document_component.html",
|
|
266
|
+
{"code": code, "rendered": rendered, "html": html},
|
|
267
|
+
)
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-cotton-bs5
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Bootstrap 5 components for use with Django Cotton.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Sam
|
|
7
7
|
Author-email: samuel.scott.jennings@gmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.12,<4.00
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Framework :: Pytest
|
|
9
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
12
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Provides-Extra: django-compressor
|
|
15
|
+
Requires-Dist: beautifulsoup4 (>=4.14.3,<5.0.0)
|
|
15
16
|
Requires-Dist: django-compressor (>=4.5.1,<5.0.0) ; extra == "django-compressor"
|
|
16
|
-
Requires-Dist: django-cotton (>=2.3.
|
|
17
|
-
Requires-Dist: django-libsass (>=0.9,<0.10)
|
|
17
|
+
Requires-Dist: django-cotton (>=2.6.1,<3.0.0)
|
|
18
|
+
Requires-Dist: django-libsass (>=0.9,<0.10) ; extra == "django-compressor"
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
20
21
|
# Django Cotton BS5
|
|
@@ -62,6 +63,90 @@ The following Bootstrap 5 components are currently available as Django Cotton co
|
|
|
62
63
|
|
|
63
64
|
More components are planned. Please request additional Bootstrap 5 components or features via the [issue tracker](https://github.com/SamuelJennings/cotton-bs5/issues).
|
|
64
65
|
|
|
66
|
+
## Testing Components
|
|
67
|
+
|
|
68
|
+
This package provides four pytest fixtures for testing Django Cotton components:
|
|
69
|
+
|
|
70
|
+
### `cotton_render` Fixture
|
|
71
|
+
|
|
72
|
+
Renders a component and returns the raw HTML as a string.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
def test_alert_component(cotton_render):
|
|
76
|
+
html = cotton_render(
|
|
77
|
+
'cotton_bs5.alert',
|
|
78
|
+
message="Hello World",
|
|
79
|
+
variant="success"
|
|
80
|
+
)
|
|
81
|
+
assert 'alert-success' in html
|
|
82
|
+
assert 'Hello World' in html
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `cotton_render_soup` Fixture
|
|
86
|
+
|
|
87
|
+
Renders a component and returns a BeautifulSoup parsed HTML object for easier DOM traversal and assertions.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
def test_alert_component(cotton_render_soup):
|
|
91
|
+
soup = cotton_render_soup(
|
|
92
|
+
'cotton_bs5.alert',
|
|
93
|
+
message="Hello World",
|
|
94
|
+
variant="success"
|
|
95
|
+
)
|
|
96
|
+
alert_div = soup.find('div', class_='alert')
|
|
97
|
+
assert 'alert-success' in alert_div['class']
|
|
98
|
+
assert alert_div.get_text() == 'Hello World'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `cotton_render_string` Fixture
|
|
102
|
+
|
|
103
|
+
Compiles and renders template strings containing Cotton component syntax. Useful for testing multi-component markup and complex layouts inline without creating separate template files.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
def test_button_inline(cotton_render_string):
|
|
107
|
+
html = cotton_render_string("<c-button variant='primary'>Click me</c-button>")
|
|
108
|
+
assert 'btn-primary' in html
|
|
109
|
+
|
|
110
|
+
def test_nested_components(cotton_render_string):
|
|
111
|
+
html = cotton_render_string(
|
|
112
|
+
"<c-ul><c-li text='first' /><c-li text='second' /></c-ul>"
|
|
113
|
+
)
|
|
114
|
+
assert 'first' in html
|
|
115
|
+
assert 'second' in html
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `cotton_render_string_soup` Fixture
|
|
119
|
+
|
|
120
|
+
Combines `cotton_render_string` with BeautifulSoup parsing for easier DOM traversal and assertions on multi-component structures.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
def test_nested_list(cotton_render_string_soup):
|
|
124
|
+
soup = cotton_render_string_soup(
|
|
125
|
+
"<c-ul><c-li text='first' /><c-li text='second' /></c-ul>"
|
|
126
|
+
)
|
|
127
|
+
items = soup.find_all('li')
|
|
128
|
+
assert len(items) == 2
|
|
129
|
+
assert items[0].get_text() == 'first'
|
|
130
|
+
|
|
131
|
+
def test_complex_layout(cotton_render_string_soup):
|
|
132
|
+
template = '''
|
|
133
|
+
<c-card>
|
|
134
|
+
<c-card.title>{{ title }}</c-card.title>
|
|
135
|
+
<c-card.body>
|
|
136
|
+
<c-button variant='primary'>{{ action }}</c-button>
|
|
137
|
+
</c-card.body>
|
|
138
|
+
</c-card>
|
|
139
|
+
'''
|
|
140
|
+
soup = cotton_render_string_soup(template, context={
|
|
141
|
+
'title': 'My Card',
|
|
142
|
+
'action': 'Click Here'
|
|
143
|
+
})
|
|
144
|
+
assert soup.find('h5').get_text() == 'My Card'
|
|
145
|
+
assert 'btn-primary' in soup.find('button')['class']
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
All fixtures automatically inject a request object, so you don't need to create one manually.
|
|
149
|
+
|
|
65
150
|
## Contributing
|
|
66
151
|
|
|
67
152
|
This library follows django-cotton conventions and Bootstrap 5 standards. When adding new components:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
cotton_bs5/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
cotton_bs5/apps.py,sha256=IwPyZAgGWygK8LpsA2_Yz4lUjyYhzEYaBL_-KVR9KK8,94
|
|
3
|
+
cotton_bs5/fixtures.py,sha256=XrUKC2p-_HoTHW-YNAoO24aYNd9pAh-hzPFcmeI5-GE,7502
|
|
3
4
|
cotton_bs5/static/_variables.scss,sha256=B5jORYuNv-a6xxsnnqj5u_JgS_qjEWySneZCInancy4,1906
|
|
4
5
|
cotton_bs5/static/bs5/_accordion.scss,sha256=-puTNym6ujT9oyWBBCKJx4xrE4Vm7Dknay6zlbgqyOw,5077
|
|
5
6
|
cotton_bs5/static/bs5/_alert.scss,sha256=3Cl_x0tBd-YWCTzPJnjhzzzxWBUTdj8yq5WV80VLD94,2073
|
|
@@ -96,14 +97,14 @@ cotton_bs5/static/bs5/vendor/_rfs.scss,sha256=JsQPrGMGFQNZ-LCv25Rv_rWHI_fX1koYba
|
|
|
96
97
|
cotton_bs5/static/stylesheet.scss,sha256=8czc7O81g7KUDSG4hVfcz_5YdYDOhEurn1uqgeQ-sLI,2529
|
|
97
98
|
cotton_bs5/templates/cotton/accordion/body.html,sha256=Y-lw1saKRb31k8s1kLyCarKNWMcVp7eCjHsV8qtRGW4,219
|
|
98
99
|
cotton_bs5/templates/cotton/accordion/header.html,sha256=B38j90UQ_Q5PXKRduFfCToOV08gFPVTK8EbF4DUNyKw,442
|
|
99
|
-
cotton_bs5/templates/cotton/accordion/index.html,sha256=
|
|
100
|
-
cotton_bs5/templates/cotton/accordion/item.html,sha256=
|
|
100
|
+
cotton_bs5/templates/cotton/accordion/index.html,sha256=a-QTsbHCiHeEXA_HzN5z7oYRMejlNVCGc7zwhuHqTTI,178
|
|
101
|
+
cotton_bs5/templates/cotton/accordion/item.html,sha256=IbRIfpcLTTimI5tPAhXjkM0jlLh_B5ICYqLjvSNG6OE,480
|
|
101
102
|
cotton_bs5/templates/cotton/alert.html,sha256=PfRSBUW4cFvx6lmTXzpP8R0JDGvPX7k1i5jMjD1YiuY,386
|
|
102
103
|
cotton_bs5/templates/cotton/badge.html,sha256=7B84yZIZWhtjdNsoeM6-FMiZEsTyTtauzIzMSws3fXU,303
|
|
103
|
-
cotton_bs5/templates/cotton/breadcrumbs/index.html,sha256=
|
|
104
|
-
cotton_bs5/templates/cotton/breadcrumbs/item.html,sha256=
|
|
104
|
+
cotton_bs5/templates/cotton/breadcrumbs/index.html,sha256=P4Q1TaGXoXOCoK-vtWmZqPAp31UTSmN6D50gN1IJFLs,370
|
|
105
|
+
cotton_bs5/templates/cotton/breadcrumbs/item.html,sha256=aeIl7FUOyoEEcs9oNNtI3eDf7aFmr8HgAk7TLCj1EOQ,436
|
|
105
106
|
cotton_bs5/templates/cotton/button/dismiss.html,sha256=Q1pBNnh8mFACc73XmQzLD9TgmPToZ374sW26SNALnCI,253
|
|
106
|
-
cotton_bs5/templates/cotton/button/index.html,sha256=
|
|
107
|
+
cotton_bs5/templates/cotton/button/index.html,sha256=ASta4ny_ZQWCctdmW3Qh4B0aJ4Sgwx-PG9AJFGqRfQ4,360
|
|
107
108
|
cotton_bs5/templates/cotton/button_group.html,sha256=HuS7PDniLkVCXovonzRqFVQrGqYfezyQfq0ihTVbwM0,429
|
|
108
109
|
cotton_bs5/templates/cotton/card/body.html,sha256=V7_erMPFbYfCWeMeI9BCtiqrKpr4Qr2LGd4uxrQhkA4,162
|
|
109
110
|
cotton_bs5/templates/cotton/card/footer.html,sha256=5wop-wWc-Pj5HU8cZLNY4L7t-cEXskyz6WiECAKvOs8,87
|
|
@@ -116,21 +117,24 @@ cotton_bs5/templates/cotton/carousel/caption.html,sha256=BD4Vgdrho5T0mvvvKmtbQYI
|
|
|
116
117
|
cotton_bs5/templates/cotton/carousel/index.html,sha256=MhulHDibT3e9B4KFW-tCpLYz0ORCfjG8kfe9zBmSXYA,452
|
|
117
118
|
cotton_bs5/templates/cotton/carousel/item.html,sha256=Q_kpQa7_uyi0_0BUxTH5_tuqzF-4PHs8WnH9WpHdfkQ,142
|
|
118
119
|
cotton_bs5/templates/cotton/collapse.html,sha256=N9lcZgBa9OxcgCHgNolmU17CM5uTbxkx6kNcY-BGvlc,344
|
|
120
|
+
cotton_bs5/templates/cotton/cotton_bs5/document.html,sha256=nc71O-Ex8F3qlIhhFjdqObkaLmK7CArL9IIi-8UiDOI,165
|
|
119
121
|
cotton_bs5/templates/cotton/dropdown/divider.html,sha256=EuKZtrn23FJSIXy1VNb96Ft1aZTNzAzEoSrehZsiGtI,32
|
|
120
122
|
cotton_bs5/templates/cotton/dropdown/header.html,sha256=vi27RD2jKOvJCdLqceQtB1PaJLDkrRoikpkue0DGA-A,83
|
|
121
123
|
cotton_bs5/templates/cotton/dropdown/index.html,sha256=tana4NotkE2h3Q0tRNS94TMc8NcgDlLMO5EdgyyH9FI,290
|
|
122
|
-
cotton_bs5/templates/cotton/dropdown/item.html,sha256=
|
|
124
|
+
cotton_bs5/templates/cotton/dropdown/item.html,sha256=a3_iPn7MixG8N47743RLofMKzvacBfCwsYxUaAuMhuo,492
|
|
123
125
|
cotton_bs5/templates/cotton/dropdown/toggle.html,sha256=cZDrs80yuwyMk3bCV2hGn5XycyC1WT6k7NeMYxxi0Mk,447
|
|
124
|
-
cotton_bs5/templates/cotton/grid/col.html,sha256=
|
|
125
|
-
cotton_bs5/templates/cotton/grid/index.html,sha256=
|
|
126
|
-
cotton_bs5/templates/cotton/
|
|
127
|
-
cotton_bs5/templates/cotton/list_group/
|
|
126
|
+
cotton_bs5/templates/cotton/grid/col.html,sha256=DvrOJ71hts9IktSiy8R_4HHb4vFlbAtZnfnaX7RJEJ0,272
|
|
127
|
+
cotton_bs5/templates/cotton/grid/index.html,sha256=KbcZR4i63BSdpXzSfv4B7jbKF8lbcfnAcGJx24Cj8io,455
|
|
128
|
+
cotton_bs5/templates/cotton/hstack.html,sha256=1NxBx5ccVoPEHWc1PDsraQYL0yd9z6zNF0MX_xtugBU,104
|
|
129
|
+
cotton_bs5/templates/cotton/list_group/index.html,sha256=vQ8XCi9gKtPfBBlrDTgcEXEOwyt0jPZydaQ62c4gveg,422
|
|
130
|
+
cotton_bs5/templates/cotton/list_group/item.html,sha256=uZXDWpKBynWeC5bD4HDygETj0rXlU_jB7YUgXn44r4M,485
|
|
128
131
|
cotton_bs5/templates/cotton/modal/body.html,sha256=ANNl7-edJij4ECMcjnX5Ms464yXIhgC8jEKIb1TbTYE,41
|
|
129
132
|
cotton_bs5/templates/cotton/modal/footer.html,sha256=Oxg9w02dcmPAmgGnqk1AfVsSk7LRqZigsClNAktP3_U,85
|
|
130
133
|
cotton_bs5/templates/cotton/modal/header.html,sha256=lMEqRfpR8soBRhRAG-6i4gVBCuftTnoe9GmJObwPStU,246
|
|
131
134
|
cotton_bs5/templates/cotton/modal/index.html,sha256=c_GZQV83gjJjv969GpKyxqXDoNKRVeQARdCQcA2qstw,652
|
|
132
135
|
cotton_bs5/templates/cotton/modal/title.html,sha256=lVPJDKK0Zk2U5VxL_jisnzPrCwgcVtcuTbIPDNQv690,138
|
|
133
136
|
cotton_bs5/templates/cotton/nav/index.html,sha256=9OczfhoKQGdQr6d52dODR6WxWymFhdbEXNIB4xXI6fw,262
|
|
137
|
+
cotton_bs5/templates/cotton/nav/item.html,sha256=iu34dEiR3ID-27PjVPCpEhmBiAumNQYAHzSInrtPb9Q,125
|
|
134
138
|
cotton_bs5/templates/cotton/nav/link.html,sha256=TyTdMfv6I1hhVhTNv84hAqMdebj2Glx7sjp28cPyn_o,262
|
|
135
139
|
cotton_bs5/templates/cotton/navbar/index.html,sha256=Xtk6dg_lg05kAyajRIxZIBSgWQhPDWCC84LDkytbG78,872
|
|
136
140
|
cotton_bs5/templates/cotton/navbar/link.html,sha256=fPtVWCZCFbMWvJW04I7N2A9L8Mk_-Yqfxw8Tv4_nu3s,364
|
|
@@ -156,7 +160,12 @@ cotton_bs5/templates/cotton/toast/activate.html,sha256=D-qh8lfEWoJWMKdUG0PT4jdNA
|
|
|
156
160
|
cotton_bs5/templates/cotton/toast/body.html,sha256=cneCr26QtF48gYR7kW5wa_QO0Qv5nGJDlXR6PjVueMU,71
|
|
157
161
|
cotton_bs5/templates/cotton/toast/header.html,sha256=izlXp-D5cKEIeIj297pma9XYgGNo6lkrgvHw1QscOR8,115
|
|
158
162
|
cotton_bs5/templates/cotton/toast/index.html,sha256=av7EWcLGcB90KUBCru6qwtl78PkM228n5OxuJewc4OI,304
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
cotton_bs5/templates/cotton/vstack.html,sha256=okGz8WD93e0PUR-FFI9p20r_PP5FQ9Ql8obVLMLtm64,104
|
|
164
|
+
cotton_bs5/templates/cotton_bs5/document_component.html,sha256=SLyce5I7CpBfMhuiEnkLU-O9OYbTQbi5fEjQ-_tvYzU,4826
|
|
165
|
+
cotton_bs5/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
166
|
+
cotton_bs5/templatetags/cotton_bs5.py,sha256=4UreJ8ZW2_L2Xfd3EKLUKlP7tHh53TH6VZ6TwDmCYNA,9128
|
|
167
|
+
django_cotton_bs5-0.6.0.dist-info/LICENSE,sha256=jAcYHTznehUzk6dbbjLdSq9Xo2S_OmscgUKA94hUoMo,1072
|
|
168
|
+
django_cotton_bs5-0.6.0.dist-info/METADATA,sha256=FK2Ob4-rLyvSM6Ss-wdc7DYgLYRxzbPpJitnFZez2QU,5853
|
|
169
|
+
django_cotton_bs5-0.6.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
170
|
+
django_cotton_bs5-0.6.0.dist-info/entry_points.txt,sha256=5FA9XkZ8EBll5jE94vwF1gp7xD1F9HTdhdKutznbTSU,43
|
|
171
|
+
django_cotton_bs5-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|