Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more tests #6

Merged
merged 109 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
f0fc1f6
Move example component to keep handling simpler
tbrlpld Dec 5, 2023
4bf808b
Rename tempalte and variables to express what they test
tbrlpld Dec 5, 2023
f5f5365
Test rendering of static template
tbrlpld Dec 5, 2023
fc3aa9e
Test component that passes fixed value to template context
tbrlpld Dec 5, 2023
fbd4903
Flip order of methods in Component to higher level at top
tbrlpld Dec 10, 2023
4c1bd4f
Add test for the basic Component class
tbrlpld Dec 10, 2023
e598382
Add test for media property
tbrlpld Dec 10, 2023
aa379eb
Add test for subclass that defines a template name
tbrlpld Dec 10, 2023
f434c3b
Create example template in test code
tbrlpld Dec 10, 2023
2184738
Test for SafeString
tbrlpld Dec 10, 2023
939ac36
Include randomness in example template file names
tbrlpld Dec 10, 2023
c3fe1c2
Test rendering with context value
tbrlpld Dec 10, 2023
ee5076e
Test render_html when get_context_data returns None
tbrlpld Dec 10, 2023
64a84b2
Remove empty models file
tbrlpld Dec 10, 2023
77751af
Add method docstring
tbrlpld Dec 11, 2023
396d06d
Use Markdown in docstring over reStructuredText
tbrlpld Dec 11, 2023
26878bb
Add first media container test
tbrlpld Dec 11, 2023
2f5dd70
Add second test for media container
tbrlpld Dec 11, 2023
5caaf10
Test empty container
tbrlpld Dec 11, 2023
f0824c7
Test members of different classes
tbrlpld Dec 11, 2023
5cc1c5c
Expand `MediaContainer` docstring
tbrlpld Dec 11, 2023
d1c14fa
Test nested Media class def in Component subclass
tbrlpld Dec 11, 2023
3149f3b
Remove reliance on os_helper module
tbrlpld Dec 11, 2023
649f8a8
Add test docstring
tbrlpld Dec 11, 2023
37b0de4
Add test with only mock component in context
tbrlpld Dec 19, 2023
2aa7b1e
Test other variable in context
tbrlpld Dec 19, 2023
af05b78
Test with sets extra context
tbrlpld Dec 19, 2023
c324390
Rename context variable to avoid shadowing
tbrlpld Dec 19, 2023
bff31c2
Test with block sets extra context
tbrlpld Dec 19, 2023
288f58c
Refactor helper
tbrlpld Dec 19, 2023
6ac1ca3
Test with-only keyword limits context
tbrlpld Dec 19, 2023
c8d3d31
Test with block overrides outer context
tbrlpld Dec 19, 2023
260ec30
Test with keyword overrides context
tbrlpld Dec 19, 2023
d108d31
Test with keyword overrides with block
tbrlpld Dec 19, 2023
3b45a65
Remove tests that have been replaced
tbrlpld Dec 19, 2023
14bf180
Use spec on mock to be able to test alternatives paths
tbrlpld Dec 19, 2023
4bedfe8
Change mock to only mock the `render_html` method
tbrlpld Dec 21, 2023
b0d1b2c
Test output in parent template
tbrlpld Dec 21, 2023
cfc48e3
Test fallback render method
tbrlpld Dec 21, 2023
4746af9
Rename test methods for clarity
tbrlpld Dec 21, 2023
606ef2d
Refactor string escaping test
tbrlpld Dec 21, 2023
3f62f9d
Relocate escaping test
tbrlpld Dec 21, 2023
0821c0e
Incorporate error representation test in other test
tbrlpld Dec 21, 2023
1876a0f
Refactor test for as-keyword
tbrlpld Dec 21, 2023
c16491f
Test return value is formatted html
tbrlpld Dec 21, 2023
5488ed7
Test render_html return not escaped when real template
tbrlpld Dec 21, 2023
08a0928
Test ability to disable autoescaping
tbrlpld Dec 21, 2023
85f6d86
Update docstring of ComponentNode.render
tbrlpld Dec 21, 2023
279c5ed
Install coverage
tbrlpld Dec 21, 2023
83c9dbd
Add basic coverage instructions to README
tbrlpld Dec 22, 2023
cadde94
Add codecov steps to CI
tbrlpld Dec 22, 2023
0ffe209
Explicitly name .coverage file
tbrlpld Dec 22, 2023
d04e01d
Test tag with no arguments
tbrlpld Dec 28, 2023
8ed41d0
Test tag parsing with unknown flag
tbrlpld Dec 28, 2023
eea9ff2
Show missing in coverage report
tbrlpld Dec 28, 2023
5a11972
Make assertions more accurate
tbrlpld Dec 28, 2023
1f6d13a
Move comment above block to which it applies
tbrlpld Dec 28, 2023
122bcb2
Test `as` keyword without following variable name
tbrlpld Dec 28, 2023
5b69da6
Test tag with unknown bit
tbrlpld Dec 28, 2023
6113db5
Change variable name for clarity
tbrlpld Dec 28, 2023
f95731e
Enable combined coverage report for tox environments
tbrlpld Dec 28, 2023
addb60b
Combine reports in GitHub Actions
tbrlpld Dec 28, 2023
cb9d301
Revert "Combine reports in GitHub Actions"
tbrlpld Dec 28, 2023
49a9ba4
Convert coverage report to json for CodeCov.io
tbrlpld Dec 28, 2023
44d2e62
Move tox to testing dependencies
tbrlpld Dec 28, 2023
5e28a35
Remove the parallel coverage option to enable json upload
tbrlpld Dec 28, 2023
715fd8d
Use append to combine coverage over different tox environments
tbrlpld Dec 28, 2023
31795f5
Move json conversion into tox commands
tbrlpld Dec 28, 2023
ac9f3c8
Add coverage back to explicit tox dependencies
tbrlpld Dec 28, 2023
c25154f
Add coverage comment
tbrlpld Dec 28, 2023
8b8cad6
Add CodeCov badge to README
tbrlpld Dec 28, 2023
3d9ec89
Move json conversion from commands to post commands
tbrlpld Dec 28, 2023
9a8d336
Rename test.home app to test.example app
tbrlpld Dec 30, 2023
c214464
Move example components from app to module
tbrlpld Dec 30, 2023
afb66a6
Rename "home" to "kitchen sink"
tbrlpld Dec 30, 2023
3bd4612
Make first example in README complete
tbrlpld Dec 31, 2023
e6d660a
Fix example formatting
tbrlpld Dec 31, 2023
87e44f6
Add test for kitchen sink view
tbrlpld Dec 31, 2023
767898b
Further clarify basic example explanations
tbrlpld Dec 31, 2023
3ee0455
Add example with overriden render_html
tbrlpld Jan 1, 2024
38be3bd
Simplify render_html override
tbrlpld Jan 1, 2024
e1baa53
Make language in README examples more consistent
tbrlpld Jan 1, 2024
0e41bce
Add example passing instance attribute to context
tbrlpld Jan 1, 2024
dd0cb04
Test dataclass as dict as context
tbrlpld Jan 1, 2024
efc293f
Make template proper html doc
tbrlpld Jan 1, 2024
1cebc37
Example passing name from parent context
tbrlpld Jan 1, 2024
a83ea61
Add example setting name with with-block in parent
tbrlpld Jan 1, 2024
381610c
Add example setting name with with-keyword
tbrlpld Jan 1, 2024
1fea6c7
Fix typo
tbrlpld Jan 1, 2024
035ae61
Remove somewhat misleading example
tbrlpld Jan 1, 2024
9077da6
Add example component with child components
tbrlpld Jan 1, 2024
94bad4f
Add example component with list of child components
tbrlpld Jan 1, 2024
a87bfca
Example that passes self to the context
tbrlpld Jan 1, 2024
bd16d98
Fix typo
tbrlpld Jan 1, 2024
cc8266a
Disabmbiguate dostring
tbrlpld Jan 1, 2024
8621880
Add explaining comment
tbrlpld Jan 1, 2024
e5ba47c
Fix typo
tbrlpld Jan 1, 2024
74d55c3
Ensure test matches name
tbrlpld Jan 1, 2024
bb5950e
Add docstrings for example app modules
tbrlpld Jan 2, 2024
c56a297
Remove useless test docstrings
tbrlpld Jan 2, 2024
85059f0
Add extra assertions for clarity
tbrlpld Jan 2, 2024
cbc91e9
More comments and docstrings
tbrlpld Jan 2, 2024
ab408d5
Fix typo and clarify comments and docstrings
tbrlpld Jan 2, 2024
c55aeb3
Clarify test docstring
tbrlpld Jan 2, 2024
fd514a3
Swap statements for better clarity and consistency with other tests
tbrlpld Jan 5, 2024
93326a3
Clarify comment
tbrlpld Jan 5, 2024
3948d0d
Clarify comment
tbrlpld Jan 5, 2024
c1f96ff
Remove extra blank line
tbrlpld Jan 5, 2024
221ecff
Update CHANGELOG
tbrlpld Jan 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ exclude_lines =
if __name__ == .__main__.:

ignore_errors = True
show_missing = True
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ jobs:
run: tox
env:
DB: sqlite
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./.coverage.json
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ __pycache__/
/dist
/laces.egg-info
/.coverage
/.coverage.json
/htmlcov
/.tox
/.venv
Expand Down
122 changes: 88 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![PyPI version](https://badge.fury.io/py/laces.svg)](https://badge.fury.io/py/laces)
[![laces CI](https://github.com/tbrlpld/laces/actions/workflows/test.yml/badge.svg)](https://github.com/tbrlpld/laces/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/tbrlpld/laces/graph/badge.svg?token=FMHEHNVPSX)](https://codecov.io/gh/tbrlpld/laces)

---

Expand Down Expand Up @@ -50,8 +51,7 @@ That's it.

### Creating components

The preferred way to create a component is to define a subclass of `laces.components.Component` and specify a `template_name` attribute on it.
The rendered template will then be used as the component's HTML representation:
The simplest way to create a component is to define a subclass of `laces.components.Component` and specify a `template_name` attribute on it.

```python
# my_app/components.py
Expand All @@ -60,19 +60,54 @@ from laces.components import Component


class WelcomePanel(Component):
template_name = "my_app/panels/welcome.html"
template_name = "my_app/components/welcome.html"
```

```html+django
{# my_app/templates/my_app/components/welcome.html #}

<h1>Welcome to my app!</h1>
```

With the above in place, you then instantiate the component (e.g. in a view) and pass it to another template for rendering.

```python
# my_app/views.py

my_welcome_panel = WelcomePanel()
from django.shortcuts import render

from my_app.components import WelcomePanel


def home(request):
welcome = WelcomePanel() # <-- Instantiates the component
return render(
request,
"my_app/home.html",
{"welcome": welcome}, # <-- Passes the component to the view template
)
```

In the view template, we `load` the `laces` tag library and use the `component` tag to render the component.

```html+django
{# my_app/templates/my_app/panels/welcome.html #}
{# my_app/templates/my_app/home.html #}

<h1>Welcome to my app!</h1>
{% load laces %}
{% component welcome %}
```

For simple cases that don't require a template, the `render_html` method can be overridden instead:
That's it!
The component's template will be rendered right there in the view template.

Of course, this is a very simple example and not much more useful than using a simple `include`.
We will go into some more useful use cases below.
tbrlpld marked this conversation as resolved.
Show resolved Hide resolved

### Without a template

Before we dig deeper into the component use cases, just a quick note that components don't have to have a template.
For simple cases that don't require a template, the `render_html` method can be overridden instead.
If the return value contains HTML, it should be marked as safe using `django.utils.html.format_html` or `django.utils.safestring.mark_safe`.

```python
# my_app/components.py
Expand All @@ -82,11 +117,11 @@ from laces.components import Component


class WelcomePanel(Component):
def render_html(self, parent_context):
return format_html("<h1>{}</h1>", "Welcome to my app!")
def render_html(self, parent_context=None):
return format_html("<h1>Welcome to my app!</h1>")
```

### Passing context to the template
### Passing context to the component template
tbrlpld marked this conversation as resolved.
Show resolved Hide resolved

The `get_context_data` method can be overridden to pass context variables to the template.
As with `render_html`, this receives the context dictionary from the calling template.
Expand All @@ -98,7 +133,7 @@ from laces.components import Component


class WelcomePanel(Component):
template_name = "my_app/panels/welcome.html"
template_name = "my_app/components/welcome.html"

def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
Expand All @@ -107,7 +142,7 @@ class WelcomePanel(Component):
```

```html+django
{# my_app/templates/my_app/panels/welcome.html #}
{# my_app/templates/my_app/components/welcome.html #}

<h1>Welcome to my app, {{ username }}!</h1>
```
Expand All @@ -123,7 +158,7 @@ from laces.components import Component


class WelcomePanel(Component):
template_name = "my_app/panels/welcome.html"
template_name = "my_app/components/welcome.html"

class Media:
css = {"all": ("my_app/css/welcome-panel.css",)}
Expand All @@ -134,7 +169,7 @@ class WelcomePanel(Component):
The `laces` tag library provides a `{% component %}` tag for including components on a template.
This takes care of passing context variables from the calling template to the component (which would not be the case for a basic `{{ ... }}` variable tag).

For example, given the view passes an instance of `WelcomePanel` to the context of `my_app/welcome.html`.
For example, given the view passes an instance of `WelcomePanel` to the context of `my_app/home.html`.

```python
# my_app/views.py
Expand All @@ -144,45 +179,45 @@ from django.shortcuts import render
from my_app.components import WelcomePanel


def welcome_page(request):
panel = (WelcomePanel(),)
def home(request):
welcome = WelcomePanel()

return render(
request,
"my_app/welcome.html",
"my_app/home.html",
{
"panel": panel,
"welcome": welcome,
},
)
```

The template `my_app/templates/my_app/welcome.html` could render the panel as follows:
The template `my_app/templates/my_app/home.html` could render the welcome panel component as follows:

```html+django
{# my_app/templates/my_app/welcome.html #}
{# my_app/templates/my_app/home.html #}

{% load laces %}
{% component panel %}
{% component welcome %}
```

You can pass additional context variables to the component using the keyword `with`:

```html+django
{% component panel with username=request.user.username %}
{% component welcome with username=request.user.username %}
```

To render the component with only the variables provided (and no others from the calling template's context), use `only`:

```html+django
{% component panel with username=request.user.username only %}
{% component welcome with username=request.user.username only %}
```

To store the component's rendered output in a variable rather than outputting it immediately, use `as` followed by the variable name:

```html+django
{% component panel as panel_html %}
{% component welcome as welcome_html %}

{{ panel_html }}
{{ welcome_html }}
```

Note that it is your template's responsibility to output any media declarations defined on the components.
Expand All @@ -197,28 +232,28 @@ from django.shortcuts import render
from my_app.components import WelcomePanel


def welcome_page(request):
panels = [
def home(request):
components = [
WelcomePanel(),
]

media = Media()
for panel in panels:
media += panel.media
for component in components:
media += component.media

render(
request,
"my_app/welcome.html",
"my_app/home.html",
{
"panels": panels,
"components": components,
"media": media,
},
)
```


```html+django
{# my_app/templates/my_app/welcome.html #}
{# my_app/templates/my_app/home.html #}

{% load laces %}

Expand All @@ -227,8 +262,8 @@ def welcome_page(request):
{{ media.css }}
<head>
<body>
{% for panel in panels %}
{% component panel %}
{% for comp in components %}
{% component comp %}
{% endfor %}
</body>
```
Expand Down Expand Up @@ -303,6 +338,25 @@ $ tox -e interactive

You can now visit `http://localhost:8020/`.

#### Testing with coverage

To run tests with coverage, use:

```sh
$ coverage run ./testmanage.py test
```

Then see the results with

```sh
$ coverage report
```

When the tests are run with `tox`, the coverage report is combined for all environments.
This is done by using the `--apend` flag when running coverage in `tox`.
This means it will also include previous results.
To get a clean report, you can run `coverage erase` before running `tox`.

### Python version management

Tox will attempt to find installed Python versions on your machine.
Expand Down
35 changes: 28 additions & 7 deletions laces/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ class Component(metaclass=MediaDefiningClass):

Extracted from Wagtail. See:
https://github.com/wagtail/wagtail/blob/094834909d5c4b48517fd2703eb1f6d386572ffa/wagtail/admin/ui/components.py#L8-L22 # noqa: E501
"""

def get_context_data(
self, parent_context: MutableMapping[str, Any]
) -> MutableMapping[str, Any]:
return {}
A component uses the `MetaDefiningClass` metaclass to add a `media` property, which
allows the definitions of CSS and JavaScript assets that are associated with the
component. This is practically the same as `Media` class used by Django forms.
See also: https://docs.djangoproject.com/en/4.2/topics/forms/media/
"""

def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str:
"""
Return string representation of the object.

Given a context dictionary from the calling template (which may be a
`django.template.Context` object or a plain ``dict`` of context variables),
`django.template.Context` object or a plain `dict` of context variables),
returns the string representation to be rendered.

This will be subject to Django's HTML escaping rules, so a return value
Expand All @@ -39,18 +39,39 @@ def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str:
template = get_template(self.template_name)
return template.render(context_data)

def get_context_data(
tbrlpld marked this conversation as resolved.
Show resolved Hide resolved
self, parent_context: MutableMapping[str, Any]
) -> MutableMapping[str, Any]:
return {}


class MediaContainer(list):
"""
A list that provides a ``media`` property that combines the media definitions
A list that provides a `media` property that combines the media definitions
of its members.

Extracted from Wagtail. See:
https://github.com/wagtail/wagtail/blob/ca8a87077b82e20397e5a5b80154d923995e6ca9/wagtail/admin/ui/components.py#L25-L36 # noqa: E501

The `MediaContainer` functionality depends on the `django.forms.widgets.Media`
class. The `Media` class provides the logic to combine the media definitions of
multiple objects through its `__add__` method. The `MediaContainer` relies on this
functionality to provide a `media` property that combines the media definitions of
its members.

See also:
https://docs.djangoproject.com/en/4.2/topics/forms/media
"""

@property
def media(self):
"""
Return a `Media` object containing the media definitions of all members.

This makes use of the `Media.__add__` method, which combines the media
definitions of two `Media` objects.

"""
media = Media()
for item in self:
media += item.media
Expand Down
File renamed without changes.
32 changes: 24 additions & 8 deletions laces/templatetags/laces.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,28 @@ def __init__(
self.target_var = target_var

def render(self, context: template.Context) -> str:
# Render a component by calling its render_html method, passing request and context from the
# calling template.
# If fallback_render_method is true, objects without a render_html method will have render()
# called instead (with no arguments) - this is to provide deprecation path for things that have
# been newly upgraded to use the component pattern.
"""
Render the ComponentNode template node.

The rendering is done by rendering the passed component by calling its
`render_html` method and passing context from the calling template.

If the passed object does not have a `render_html` method but a `render` method
and the `fallback_render_method` arguments of the template tag is true, then
the `render` method is used. The `render` method does not receive any arguments.

Additional context variables can be passed to the component by using the `with`
keyword. The `with` keyword accepts a list of key-value pairs. The key is the
name of the context variable and the value is the value of the context variable.

The `only` keyword can be used to isolate the context. This means that the
context variables from the parent context are not passed to the component the
only context variables passed to the component are the ones passed with the
`with` keyword.

The `as` keyword can be used to store the rendered component in a variable
in the parent context. The variable name is passed after the `as` keyword.
"""
component = self.component.resolve(context)

if self.fallback_render_method:
Expand Down Expand Up @@ -84,9 +100,9 @@ def component(parser, token):

# the only valid keyword argument immediately following the component
# is fallback_render_method
flags = token_kwargs(bits, parser)
fallback_render_method = flags.pop("fallback_render_method", None)
if flags:
kwargs = token_kwargs(bits, parser)
fallback_render_method = kwargs.pop("fallback_render_method", None)
if kwargs:
raise template.TemplateSyntaxError(
"'component' tag only accepts 'fallback_render_method' as a keyword argument"
)
Expand Down
1 change: 0 additions & 1 deletion laces/test/components/__init__.py

This file was deleted.

Loading