Skip to content

Commit 5916a34

Browse files
committed
Big cleanup
1 parent f115d46 commit 5916a34

17 files changed

Lines changed: 134 additions & 57 deletions

File tree

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ lint:
1111
uv run ruff check src tests
1212
uv run ty check
1313

14+
.PHONY: lintfix
15+
lintfix:
16+
uv run ruff check src tests --fix
17+
1418
.PHONY: coverage
1519
coverage:
1620
uv run pytest --cov=formidable --cov-config=pyproject.toml --cov-report=html tests

docs/content/fields/form.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ title: FormField
55
::: api formidable.FormField
66
:show_members: false
77
:::
8+
89
----
910

1011
This is a form field that contains another form as its value. For example:
@@ -28,22 +29,22 @@ class Profile(f.Form):
2829

2930
To the user, this will probably look as part of the same form, why bother then? Well, this field isn't about showing the form, it's about how the data is saved.
3031

31-
When saving a form with a `FormField`, the contents of it comes encapsulated in their own object or dictionary:
32+
When saving a form with a `FormField`, the contents of it come encapsulated in their own object or dictionary:
3233

3334
```python
3435
print(form.save())
3536

3637
{
3738
"name": "My name",
3839
"settings": {
39-
"locale": "en_us"
40+
"locale": "en_us",
4041
"timezone": "utc",
4142
"email_notifications": True,
4243
},
4344
}
4445
```
4546

46-
This field is useful when you want to store those group of fields separated, for example:
47+
This field is useful when you want to store that group of fields separated, for example:
4748

4849
In a different model with a one-to-one relationship to the main model
4950

docs/content/forms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ print(product.user_id) # 123
196196

197197
If you add an `after_validate` method to the form, it will be called at the end of the validation process, after the individual field validations.
198198

199-
You can use it to validate the relation between fields, for example in a update password scenario, or to modify the field values before saving.
199+
You can use it to validate the relation between fields, for example in an update password scenario, or to modify the field values before saving.
200200

201201
```python {hl_lines="11"}
202202
import formidable as f

docs/content/messages.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,30 @@ You can see the full (short) default dictionary of error messages in `formidable
2626

2727
```python
2828
MESSAGES = {
29-
"invalid": "Invalid value",
30-
"required": "Field is required",
31-
"one_of": "Must be one of {one_of}",
32-
"gt": "Must be greater than {gt}",
33-
"gte": "Must be greater or equal than {gte}",
34-
"lt": "Must be less than {lt}",
35-
"lte": "Must be less or equal than {lte}",
36-
"multiple_of": "Must multiple of {multiple_of}",
37-
"min_items": "Must have at least {min_length} items",
38-
"max_items": "Must have at most {max_length} items",
39-
"min_length": "Must have at least {min_length} characters",
40-
"max_length": "Must have at most {max_length} characters",
41-
"pattern": "Invalid format",
42-
"past_date": "Must be a date in the past",
43-
"future_date": "Must be a date in the future",
44-
"after_date": "Must be after {after_date}",
45-
"before_date": "Must be before {before_date}",
46-
"after_time": "Must be after {after_time}",
47-
"before_time": "Must be before {before_time}",
48-
"past_time": "Must be a time in the past",
49-
"future_time": "Must be a time in the future",
50-
"invalid_url": "Doesn't seem to be a valid URL",
51-
"invalid_email": "Doesn't seem to be a valid email address",
52-
"invalid_slug": "A valid slug can only have a-z letters, numbers, underscores, or hyphens",
29+
invalid”: “Invalid value,
30+
required”: “Field is required,
31+
one_of”: “Must be one of {one_of},
32+
“gt”: “Must be greater than {gt},
33+
gte”: “Must be greater than or equal to {gte},
34+
“lt”: “Must be less than {lt},
35+
lte”: “Must be less than or equal to {lte},
36+
multiple_of”: “Must be a multiple of {multiple_of},
37+
min_items”: “Must have at least {min_items} items,
38+
max_items”: “Must have at most {max_items} items,
39+
min_length”: “Must have at least {min_length} characters,
40+
max_length”: “Must have at most {max_length} characters,
41+
pattern”: “Invalid format,
42+
past_date”: “Must be a date in the past,
43+
future_date”: “Must be a date in the future,
44+
after_date”: “Must be after {after_date},
45+
before_date”: “Must be before {before_date},
46+
after_time”: “Must be after {after_time},
47+
before_time”: “Must be before {before_time},
48+
past_time”: “Must be a time in the past,
49+
future_time”: “Must be a time in the future,
50+
invalid_url”: “Doesn't seem to be a valid URL,
51+
invalid_email”: “Doesn't seem to be a valid email address,
52+
invalid_slug”: “A valid 'slug' can only have a-z letters, numbers, underscores, or hyphens,
5353
}
5454
```
5555
:::
@@ -142,7 +142,7 @@ class NewPasswordForm(f.Form):
142142

143143
def validate_password(self, value):
144144
if "&" not in value:
145-
raise ValueError("must_contain", char="&")
145+
raise ValueError("must_contain", {"char": "&"})
146146
return value
147147

148148

docs/content/nested.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ for example, to the `static/js/` folder.
196196

197197
Then, add this to the header of your base template:
198198

199-
```html {hk_lines="4-8"}
199+
```html {hl_lines="4-8"}
200200
<html>
201201
<head>
202202
...
@@ -219,7 +219,7 @@ class TodoListForm(f.Form):
219219
todo = f.NestedForms(TodoForm, allow_delete=True)
220220
```
221221

222-
... and a template. I'm using a macro to render the `TodoForm` (because later we are going to need to render it more than once:
222+
... and a template. I'm using a macro to render the `TodoForm` (because later we are going to need to render it more than once):
223223

224224
```html+jinja
225225
{% macro render_todo(form, label) -%}
@@ -301,7 +301,7 @@ The JS script needs something to clone so it can add a new nested form, let's ad
301301
</form>
302302
```
303303

304-
**Note that we call the macro using `form.todo.empty_form`**. This is a special attribute of a `NestedForms` field that generates an empty instance of a nested form, excatly for using it for this cases.
304+
**Note that we call the macro using `form.todo.empty_form`**. This is a special attribute of a `NestedForms` field that generates an empty instance of a nested form, exactly for using it for these cases.
305305

306306
It's important to put it *inside* the element with the `data-nestedform` attribute (the form tag in our example).
307307

docs/content/orm.md

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ After calling `form.save()`, you only need to commit the changes to the database
3131

3232
### SQLAlchemy and SQLModel
3333

34-
Because these ORMs work with a session pattern, you must add these two methods to a base form:
34+
Because these ORMs work with a session pattern, Formidable's default `ObjectManager` needs to be extended. There are two ways to do this:
3535

36-
```python {title="forms/base.py"}
37-
import formidable as f
36+
#### Option A: Add methods to a model base class
3837

39-
class BaseForm(f.Form):
38+
Formidable's `ObjectManager` calls `orm_cls.create(**data)` when creating new objects and `object.delete()` when deleting them. You can add these methods to a shared model base class:
39+
40+
```python {title="models/base.py"}
41+
from sqlalchemy.orm import DeclarativeBase
42+
43+
class Base(DeclarativeBase):
4044
@classmethod
4145
def create(cls, **kwargs):
4246
instance = cls(**kwargs)
@@ -49,7 +53,40 @@ class BaseForm(f.Form):
4953

5054
```
5155

52-
and make all your forms inherit from it:
56+
Then use `Base` as the base for all your models:
57+
58+
```python {title="models/page.py"}
59+
class Page(Base):
60+
__tablename__ = "pages"
61+
title: Mapped[str]
62+
content: Mapped[str]
63+
64+
```
65+
66+
#### Option B: Provide a custom ObjectManager
67+
68+
Alternatively, you can subclass `ObjectManager` to handle the session directly:
69+
70+
```python {title="forms/base.py"}
71+
import formidable as f
72+
from formidable.wrappers import ObjectManager
73+
74+
class SAObjectManager(ObjectManager):
75+
def create(self, data):
76+
instance = self.orm_cls(**data)
77+
db_session.add(instance)
78+
return instance
79+
80+
# Only needed for NestedForms fields
81+
def delete(self):
82+
db_session.delete(self.object)
83+
84+
class BaseForm(f.Form):
85+
_ObjectManager = SAObjectManager
86+
87+
```
88+
89+
Then make all your forms inherit from it:
5390

5491
```python {title="forms/page.py", hl_lines="5"}
5592
import formidable as f

src/formidable/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@
6363

6464
INVALID_URL: "Doesn't seem to be a valid URL",
6565
INVALID_EMAIL: "Doesn't seem to be a valid email address",
66-
INVALID_SLUG: "A valid slug can only have a-z letters, numbers, underscores, or hyphens"
66+
INVALID_SLUG: "A valid 'slug' can only have a-z letters, numbers, underscores, or hyphens"
6767
}

src/formidable/fields/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from .email import EmailField # noqa
66
from .file import FileField # noqa
77
from .formfield import FormField # noqa
8-
from .nested import NestedForms # noqa
98
from .list import ListField # noqa
9+
from .nested import NestedForms # noqa
1010
from .number import FloatField, IntegerField # noqa
1111
from .slug import SlugField # noqa
1212
from .text import TextField # noqa

src/formidable/fields/base.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ def _custom_filter(self, value: t.Any) -> t.Any:
180180
def _custom_validator(self, value: t.Any) -> t.Any:
181181
return value
182182

183+
def _str_value(self) -> str:
184+
if hasattr(self.value, "isoformat"):
185+
return self.value.isoformat()
186+
return str(self.value)
187+
183188
# Helper methods for rendering HTML forms
184189

185190
def label(self, text: str | None = None, **attrs: t.Any) -> str:
@@ -372,16 +377,17 @@ def select(
372377
attributes.update(attrs)
373378
attr_str = self._render_html_attrs(attributes)
374379

375-
options_html = ""
376380
values = self.value if self.multiple else [self.value]
377381

382+
option_tags = []
378383
for value, display in options:
379384
selected_attr = " selected" if value in values else ""
380385
escaped_value = Markup.escape(value)
381386
escaped_display = Markup.escape(display)
382-
options_html += f'<option value="{escaped_value}"{selected_attr}>{escaped_display}</option>\n'
387+
option_tags.append(f'<option value="{escaped_value}"{selected_attr}>{escaped_display}</option>')
383388

384-
return Markup(f"<select {attr_str}>\n{options_html}</select>")
389+
options_html = "\n".join(option_tags)
390+
return Markup(f"<select {attr_str}>\n{options_html}\n</select>")
385391

386392
def checkbox(self, **attrs: t.Any) -> str:
387393
"""
@@ -491,7 +497,7 @@ def _input(self, input_type: str = "text", **attrs: t.Any) -> str:
491497
"type": input_type,
492498
"id": self.id,
493499
"name": self.name,
494-
"value": False if self.value is None else str(self.value),
500+
"value": False if self.value is None else self._str_value(),
495501
"required": self.required,
496502
}
497503
if self.error:
@@ -529,7 +535,7 @@ def hidden_input(self, **attrs: t.Any) -> str:
529535
attributes = {
530536
"type": "hidden",
531537
"name": self.name,
532-
"value": False if self.value is None else str(self.value),
538+
"value": False if self.value is None else self._str_value(),
533539
**attrs,
534540
}
535541
attr_str = self._render_html_attrs(attributes)

src/formidable/fields/date.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def validate_value(self) -> bool:
112112
"""
113113
Validate the field value against the defined constraints.
114114
"""
115-
if not self.value:
115+
if self.value is None:
116116
return True
117117

118118
if self.after_date and self.value <= self.after_date:

0 commit comments

Comments
 (0)