Skip to content

Commit

Permalink
Docs, examples, template tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
tomchristie committed Mar 5, 2019
1 parent a25efd9 commit 202166a
Show file tree
Hide file tree
Showing 19 changed files with 552 additions and 106 deletions.
162 changes: 139 additions & 23 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ The following are examples of integrating `typesystem` against a Web framework.

## API validation & serialization

Using typesystem for validation and serialization of a simple Web API.

**requirements.txt**

```
starlette
typesystem
uvicorn
```

**app.py**

```python
from starlette.applications import Starlette
from starlette.responses import JSONResponse
Expand Down Expand Up @@ -30,7 +42,7 @@ async def add_user(request):
return JSONResponse(dict(user))


app = Starlette(routes=[
app = Starlette(debug=True, routes=[
Route('/', list_users, methods=["GET"]),
Route('/', add_user, methods=["POST"]),
])
Expand All @@ -42,49 +54,85 @@ if __name__ == "__main__":

## Form rendering

Using typesystem for a simple Web submission page.

![Bootstrap example](img/boostrap_example.png)

**requirements.txt**

```
aiofiles # Static files support
boostrap4 # Form templates & static files
jinja2 # Form rendering
python-multipart # Form parsing
starlette
typesystem
uvicorn
```

**app.py**

```python
from starlette.applications import Starlette
from starlette.responses import RedirectResponse
from starlette.routing import Route, Mount
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates
import typesystem
import uvicorn


forms = typesystem.Jinja2Forms(package="bootstrap4")
templates = Jinja2Templates(directory="templates")
statics = StaticFiles(packages=["bootstrap4"])

users = []


class User(typesystem.Schema):
username = typesystem.String(max_length=100)
is_admin = typesystem.Boolean(default=False)
statics = StaticFiles(directory="statics", packages=["bootstrap4"])
bookings = []


class BookingSchema(typesystem.Schema):
start_date = typesystem.Date(title="Start date")
end_date = typesystem.Date(title="End date")
room = typesystem.Choice(
title="Room type",
choices=[
("double", "Double room"),
("twin", "Twin room"),
("single", "Single room"),
],
)
include_breakfast = typesystem.Boolean(title="Include breakfast", default=False)

def __str__(self):
breakfast = (
"(with breakfast)" if self.include_breakfast else "(without breakfast)"
)
return f"Booking for {self.room} from {self.start_date} to {self.end_date}"


async def homepage(request):
form = forms.Form(User)
return templates.TemplateResponse('index.html', {'users': users, 'form': form})
form = forms.Form(BookingSchema)
context = {"request": request, "form": form, "bookings": bookings}
return templates.TemplateResponse("index.html", context)


async def add_user(request):
async def make_booking(request):
data = await request.form()
user, errors = User.validate_or_error(data)
booking, errors = BookingSchema.validate_or_error(data)
if errors:
form = forms.Form(User, values=data, errors=errors)
return templates.TemplateResponse('index.html', {'form': form}, status_code=400)
users.append(user)
return RedirectResponse(url=request.url_for('homepage'))
form = forms.Form(BookingSchema, values=data, errors=errors)
context = {"request": request, "form": form, "bookings": bookings}
return templates.TemplateResponse("index.html", context)

bookings.append(booking)
return RedirectResponse(request.url_for("homepage"))

app = Starlette(routes=[
Route('/', homepage, methods=['GET']),
Route('/', add_user, methods=['POST']),
Mount('/static', app=statics, name='static')
])

app = Starlette(
debug=True,
routes=[
Route("/", homepage, methods=["GET"]),
Route("/", make_booking, methods=["POST"]),
Mount("/statics", statics, name="static"),
],
)


if __name__ == "__main__":
Expand All @@ -94,4 +142,72 @@ if __name__ == "__main__":
**templates/index.html**

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>TypeSystem</title>
<link href="{{ url_for('static', path='/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', path='/css/base.css') }}" rel="stylesheet">
</head>

<body>
<main role="main" class="container">
<div class="booking-list">
<h4>Current bookings</h4>
<div>
{% if bookings %}
<ul>
{% for booking in bookings %}<li>{{ booking }}</li>{% endfor %}
</ul>
{% else %}
<em>None</em>
{% endif %}
</div>
</div>
<hr/>
<div class="booking-form">
<h4>New booking</h4>
<form method="POST">
{{ form }}
<div class="submit-controls">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</main>

<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="{{ url_for('static', path='js/bootstrap.min.js') }}"></script>
</body>
</html>
```

**statics/css/base.css**

```css
.container {
padding-top: 20px;
max-width: 500px;
}

.booking-list {
padding: 20px 0 30px;
}

.booking-list li {
padding: 10px 0;
}

.booking-form form {
padding: 10px 0;
}

.submit-controls {
padding: 15px 0;
}
```
152 changes: 118 additions & 34 deletions docs/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,145 @@ import typesystem
forms = typesystem.Jinja2Forms(package="typesystem") # Use the default templates.

class BookingSchema(typesystem.Schema):
start_date = typesystem.Date()
end_date = typesystem.Date()
room = typesystem.Choice(choices=[
start_date = typesystem.Date(title="Start date")
end_date = typesystem.Date(title="End date")
room = typesystem.Choice(title="Room type", choices=[
('double', 'Double room'),
('twin', 'Twin room'),
('single', 'Single room')
])
include_breakfast = typesystem.Boolean(default=False)
include_breakfast = typesystem.Boolean(title="Include breakfast", default=False)

form = forms.Form(BookingSchema)
print(form)
```

**app.py**
That'll render an HTML form which looks something like this:

![Default form rendering](img/form_default_rendering.png)

The default templates included by `typesystem` use tables to provide a simple
functional layout.

Notice that only the fields in the form are rendered. The surrounding `<form>`, `<table>`, and `<input type="submit">` tags should be included directly in any template that renders the form.

```html
<tr>
<td>
<label for="form-bookingschema-start-date">Start date</label>
</td>
<td>
<input type="date" id="form-bookingschema-start-date" name="start_date" required>
</td>
</tr>
<tr>
<td>
<label for="form-bookingschema-end-date">End date</label>
</td>
<td>
<input type="date" id="form-bookingschema-end-date" name="end_date" required>
</td>
</tr>
<tr>
<td>
<label for="form-bookingschema-room">Room type</label>
</td>
<td>
<select id="form-bookingschema-room" name="room">
<option></option>
<option value="double">Double room</option>
<option value="twin">Twin room</option>
<option value="single">Single room</option>
</select>
</td>
</tr>
<tr>
<td>
<label for="form-bookingschema-include-breakfast">Include breakfast</label>
</td>
<td>
<input type="checkbox" id="form-bookingschema-include-breakfast" name="include_breakfast" value="true">
</td>
</tr>
```

You can include a form inside a Jinja2 template by passing it as context,
and rendering the context value. Make sure to include the surrounding tags:

```html
<form action="/" method="POST">
<table>
{{ form }}
<tr>
<td></td>
<td><input type="submit" value="Make booking"/></td>
</tr>
</table>
</form>
```

## Including values and errors

We can include values in a form like so:

```python
from starlette import templating
import typesystem
initial_values = {'room': 'double', 'include_breakfast': True}
form = forms.Form(BookingSchema, values=initial_values)
```

We can also include validation errors:

```python
booking, errors = BookingSchema.validate_or_error(data)
if errors:
form = forms.Form(BookingSchema, values=data, errors=errors)
```

## Customizing field rendering

If you'd like to override the default field rendering you can switch to
using your own custom templates. Switch this line:

```python
forms = typesystem.Jinja2Forms(package="typesystem")
templates = templating.Jinja2Templates(directory="templates")
```

async def booking_page(request):
message = request.session.get('message')
request.session.clear()
form = forms.Form(BookingSchema)
context = {'request': request, 'form': form, 'message': message}
return templates.TemplateResponse('index.html', context)
To instead use a `templates` directories:

```python
forms = typesystem.Jinja2Forms(directory="templates")
```

You'll need to provide the following templates:

async def make_booking(request):
data = await request.form()
booking, errors = BookingSchema.validate(data)
if errors:
form = forms.Form(BookingSchema, values=data, errors=errors)
context = {'request': request, 'form': form, 'message': None}
return templates.TemplateResponse('index.html', context)
* `forms/checkbox.html`
* `forms/input.html`
* `forms/select.html`
* `forms/textarea.html`

request.session['message'] = f'Booking made: {booking}')
return RedirectResponse(url='/')
## Using a packaged theme

You can also use a pre-packaged theme, such as Bootstrap 4:

app = Starlette(routes=[
Route('/', booking_page, methods=['GET'])
Route('/', make_booking, methods=['POST'])
])
```shell
# This pypi package includes `typesystem` form templates,
# and pre-packaged static files.
$ pip install bootstrap4
```

**templates/index.html**:
```python
forms = typesystem.Jinja2Forms(package="bootstrap4")
```

The `bootstrap4` package also provides the static CSS and JavaScript files,
which you can easily serve up from Starlette's `StaticFiles` application:

```python
<html>
<body>
{% if message %}<div><p>{{message}}</p></div>{% endif %}
<div>{{booking_form}}</div>
</body>
</html>
# Serve everything in the local "statics" directory plus everything
# in the "statics" directory of the `bootstrap4` package.
statics = StaticFiles(directory="statics", packages=["bootstrap4"])
```

That will then render HTML forms using Bootstrap:

![Bootstrap form rendering](img/form_boostrap_rendering.png)
Binary file added docs/img/boostrap_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/form_boostrap_rendering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/form_default_rendering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 202166a

Please sign in to comment.