Skip to content

Commit

Permalink
Merge pull request #6 from tbrlpld/add-tests
Browse files Browse the repository at this point in the history
Add more tests
  • Loading branch information
tbrlpld authored Jan 25, 2024
2 parents b58397c + 221ecff commit fe8a7ec
Show file tree
Hide file tree
Showing 31 changed files with 1,248 additions and 134 deletions.
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- ...
- Add more tests and example usage.

### Changed

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.

### 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

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 `--append` 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 works 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(
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.
Loading

0 comments on commit fe8a7ec

Please sign in to comment.