Skip to content

Commit bee2ba8

Browse files
authored
Merge pull request #8 from python-ellar/field_widget
Feat: Field Widgets
2 parents 964bdc9 + d6e24a9 commit bee2ba8

19 files changed

+672
-309
lines changed

tests/test_boolean_field.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,21 @@ class TestModel(BaseModel):
6060
def test_render_html(self):
6161
field = BooleanField(name="active")
6262
field.process(True)
63-
html = str(field.render())
63+
html = str(field())
6464
assert 'type="checkbox"' in html
6565
assert 'name="active"' in html
6666
assert "checked" in html
6767

6868
def test_render_html_with_help_text(self):
6969
field = BooleanField(name="active", help_text="Active")
70-
html = str(field.render())
70+
html = str(field())
7171
assert 'type="checkbox"' in html
7272
assert 'name="active"' in html
7373
assert "checked" not in html
7474
assert "Active" in html
7575

7676
def test_render_html_checkbox(self):
7777
field = BooleanField(name="agree")
78-
html = str(field.render())
78+
html = str(field())
7979
assert 'type="checkbox"' in html
8080
assert 'name="agree"' in html

tests/test_choice_field.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_get_render_context(self):
5151
choices = [("a", "Option A"), ("b", "Option B")]
5252
field = ChoiceField(name="test", choices=choices)
5353
field.data = "a"
54-
attrs, context = field.get_render_context({})
54+
context = field.widget.get_render_context()
5555

5656
assert context["choices"] == choices
5757
assert context["is_selected"]("a")
@@ -105,3 +105,16 @@ class TestModel(BaseModel):
105105
assert form.errors["multi_choice"] == [
106106
"Value error, Input should be in ['x', 'y', 'z']"
107107
]
108+
109+
def test_choice_field_with_choices_loader(self):
110+
def choices_loader():
111+
return [("a", "Option A"), ("b", "Option B")]
112+
113+
field = ChoiceField(name="test", choices_loader=choices_loader)
114+
assert field._choices_loader == choices_loader
115+
116+
def test_choice_field_with_choices_loader_and_choices(self, create_context):
117+
field = ChoiceField(
118+
name="test", choices_loader=lambda: [("a", "Option A"), ("b", "Option B")]
119+
)
120+
assert field._choices == [("a", "Option A"), ("b", "Option B")]

tests/test_enum_field.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_enum_field_process():
5555

5656
def test_enum_field_render():
5757
field = EnumField(enum=Color, name="color")
58-
rendered = field.render()
58+
rendered = field.widget.render()
5959
assert '<select class="" id="color" name="color" required>' in rendered
6060
assert 'name="color"' in rendered
6161
assert "<option value='red'>RED</option>" in rendered
@@ -89,7 +89,7 @@ def test_enum_field_render_multiple():
8989
field_info_args={"default": [Color.RED, Color.GREEN]},
9090
)
9191
field.load()
92-
rendered = field.render()
92+
rendered = field.widget.render()
9393
assert '<select class="" id="color" multiple name="color">' in rendered
9494
assert 'name="color"' in rendered
9595
assert "<option value='red' selected>RED</option>" in rendered

tests/test_form_manager.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from ellar.testing import Test
55

66
from zform import ZForm, FormManager
7-
from zform.fields import StringField, EmailField
7+
from zform.fields import StringField, EmailField, FieldBase
88

99

1010
class UserFormModel(BaseModel):
@@ -130,3 +130,31 @@ def login():
130130
assert response.status_code == 200
131131
assert response.json()["status"] == "error"
132132
assert "errors" in response.json()
133+
134+
135+
def test_zform_from_fields():
136+
fields = [StringField(name="username"), EmailField(name="email")]
137+
form = FormManager.from_fields(fields)
138+
assert isinstance(form, FormManager)
139+
assert len(list(form)) == 2
140+
assert all(isinstance(field, FieldBase) for field in list(form))
141+
142+
143+
def test_zform_from_fields_with_populate_form():
144+
fields = [StringField(name="username"), EmailField(name="email")]
145+
form = FormManager.from_fields(fields)
146+
form.populate_form(data={"username": "testuser", "email": "[email protected]"})
147+
assert form.get_field("username").value == "testuser"
148+
assert form.get_field("email").value == "[email protected]"
149+
150+
151+
def test_zform_from_fields_with_validate(create_context):
152+
fields = [StringField(name="username"), EmailField(name="email")]
153+
ctx = create_context({"username": "testuser", "email": "[email protected]"})
154+
form = FormManager.from_fields(fields, ctx=ctx)
155+
156+
is_valid = form.validate()
157+
158+
assert is_valid
159+
assert form.errors == {}
160+
assert form.value == {"username": "testuser", "email": "[email protected]"}

tests/test_numbers_field.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_render_html(self):
4646
step=2,
4747
field_info_args={"annotation": int},
4848
)
49-
html = field.render()
49+
html = field.widget.render()
5050
assert 'type="number"' in html
5151
assert 'name="age"' in html
5252
assert 'max="100"' in html
@@ -83,7 +83,7 @@ def test_render_html(self):
8383
step=0.1,
8484
field_info_args={"annotation": float},
8585
)
86-
html = field.render()
86+
html = field.widget.render()
8787
assert 'type="number"' in html
8888
assert 'name="weight"' in html
8989
assert 'max="100.5"' in html
@@ -134,7 +134,7 @@ def test_render_html(self):
134134
field = DecimalField(
135135
name="price", max=100, min=0, placeholder="Enter a decimal", step="0.01"
136136
)
137-
html = str(field.render())
137+
html = str(field.widget.render())
138138
assert 'type="number"' in html
139139
assert 'name="price"' in html
140140
assert 'max="100"' in html
@@ -188,7 +188,7 @@ class TestModel(BaseModel):
188188

189189
def test_render_html(self):
190190
field = RangeField(name="volume", max=100, min=0, step=5)
191-
html = field.render()
191+
html = field.widget.render()
192192
assert 'type="range"' in html
193193
assert 'name="volume"' in html
194194
assert 'max="100"' in html

tests/test_string_form_fields.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class TestTimeZoneField:
1212
def test_default_timezones(self):
13-
field = TimeZoneField(name="timezone").rebuild()
13+
field = TimeZoneField(name="timezone")
1414
assert len(field._choices) > 0
1515
assert all(isinstance(choice, tuple) for choice in field._choices)
1616

@@ -19,11 +19,11 @@ def test_custom_timezones(self):
1919
("UTC", "Coordinated Universal Time"),
2020
("GMT", "Greenwich Mean Time"),
2121
]
22-
field = TimeZoneField(name="timezone", time_zones=custom_zones).rebuild()
22+
field = TimeZoneField(name="timezone", time_zones=custom_zones)
2323
assert field._choices == custom_zones
2424

2525
def test_render(self):
26-
field = TimeZoneField(name="timezone", required=True).rebuild()
26+
field = TimeZoneField(name="timezone", required=True)
2727
rendered = str(field())
2828
assert '<select class="" id="timezone" name="timezone" required>' in rendered
2929
assert "<option value='UTC'>UTC</option>" in rendered
@@ -32,19 +32,19 @@ def test_render(self):
3232

3333
class TestDateTimeLocalField:
3434
def test_type(self):
35-
field = DateTimeLocalField(name="datetime_local").rebuild()
35+
field = DateTimeLocalField(name="datetime_local")
3636
assert field.type == "datetime-local"
3737

3838
def test_data_alt_format(self):
3939
field = DateTimeLocalField(name="datetime_local", data_alt_format="Y-m-d H:i:S")
4040
assert field.attrs["data_alt_format"] == "Y-m-d H:i:S"
4141

4242
def test_python_type(self):
43-
field = DateTimeLocalField(name="datetime_local").rebuild()
43+
field = DateTimeLocalField(name="datetime_local")
4444
assert field.python_type == datetime.datetime
4545

4646
def test_render(self):
47-
field = DateTimeLocalField(name="datetime_local").rebuild()
47+
field = DateTimeLocalField(name="datetime_local")
4848
rendered = field()
4949
assert 'type="datetime-local"' in rendered
5050
assert 'name="datetime_local"' in rendered
@@ -53,15 +53,15 @@ def test_render(self):
5353

5454
class TestDateTimeField:
5555
def test_type(self):
56-
field = DateTimeField(name="datetime").rebuild()
56+
field = DateTimeField(name="datetime")
5757
assert field.type == "datetime"
5858

5959
def test_python_type(self):
60-
field = DateTimeField(name="datetime").rebuild()
60+
field = DateTimeField(name="datetime")
6161
assert field.python_type == datetime.datetime
6262

6363
def test_render(self):
64-
field = DateTimeField(name="datetime").rebuild()
64+
field = DateTimeField(name="datetime")
6565
rendered = field()
6666
assert 'type="datetime"' in rendered
6767
assert 'name="datetime"' in rendered
@@ -70,15 +70,15 @@ def test_render(self):
7070

7171
class TestDateField:
7272
def test_type(self):
73-
field = DateField(name="date").rebuild()
73+
field = DateField(name="date")
7474
assert field.type == "date"
7575

7676
def test_python_type(self):
77-
field = DateField(name="date").rebuild()
77+
field = DateField(name="date")
7878
assert field.python_type == datetime.date
7979

8080
def test_render(self):
81-
field = DateField(name="date").rebuild()
81+
field = DateField(name="date")
8282
rendered = field()
8383
assert 'type="date"' in rendered
8484
assert 'name="date"' in rendered
@@ -87,15 +87,15 @@ def test_render(self):
8787

8888
class TestTimeField:
8989
def test_type(self):
90-
field = TimeField(name="time").rebuild()
90+
field = TimeField(name="time")
9191
assert field.type == "time"
9292

9393
def test_data_alt_format(self):
94-
field = TimeField(name="time", data_alt_format="H:i").rebuild()
94+
field = TimeField(name="time", data_alt_format="H:i")
9595
assert field.attrs["data_alt_format"] == "H:i"
9696

9797
def test_python_type(self):
98-
field = TimeField(name="time").rebuild()
98+
field = TimeField(name="time")
9999
assert field.python_type == datetime.time
100100

101101
def test_render(self):

tests/test_zform_field_list.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ def test_add_item(field_list):
2525
new_field = field_list.add_item()
2626
assert len(field_list._items) == 1
2727
assert isinstance(new_field, StringField)
28-
assert new_field.name == "xys.0"
28+
assert new_field.name == "xys"
29+
assert new_field.field_info_args.alias == "xys.0"
2930

3031

3132
def test_clear_items(field_list):
@@ -59,7 +60,8 @@ def test_extra_indices(field_list):
5960
def test_get_new_field_at(field_list):
6061
new_field = field_list._get_new_field_at("xys.2")
6162
assert isinstance(new_field, StringField)
62-
assert new_field.name == "xys.2"
63+
assert new_field.name == "xys"
64+
assert new_field.field_info_args.alias == "xys.2"
6365

6466

6567
def test_field_list_iteration(field_list):
@@ -74,8 +76,10 @@ def test_field_list_indexing(field_list):
7476
field_list.add_item()
7577
field_list.add_item()
7678
assert isinstance(field_list._items[0], StringField)
77-
assert field_list._items[0].name == "xys.0"
78-
assert field_list._items[1].name == "xys.1"
79+
assert field_list._items[0].name == "xys"
80+
assert field_list._items[0].field_info_args.alias == "xys.0"
81+
assert field_list._items[1].name == "xys"
82+
assert field_list._items[1].field_info_args.alias == "xys.1"
7983

8084

8185
def test_field_list_len(field_list):

tests/test_zform_obj_field.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def test_validate_setup():
3838

3939

4040
def test_get_render_context(object_field):
41-
attrs, context = object_field.get_render_context({})
41+
context = object_field.widget.get_render_context()
4242
assert "fields" in context
4343
assert len(context["fields"]) == 2
4444

@@ -73,8 +73,8 @@ def test_process(object_field):
7373
data = {"name": "Alice", "age": 25}
7474
object_field.process(data)
7575

76-
assert object_field._fields[0]._value == "Alice"
77-
assert object_field._fields[1]._value == 25
76+
assert object_field._fields[0].value == "Alice"
77+
assert object_field._fields[1].value == 25
7878

7979

8080
def test_python_type_with_schema(schema_object_field):

zform/fields/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from datetime import date, datetime, time
55

66
from pydantic import BaseModel, EmailStr
7-
from pydantic._internal._utils import lenient_issubclass
7+
from ellar.pydantic import lenient_issubclass
88
from typing_extensions import get_origin
9-
10-
from .base import FieldBase, FormLabel
9+
from .select import ChoiceField
10+
from .base import FieldBase
1111
from .dates import (
1212
DateField,
1313
DateTimeField,
@@ -37,7 +37,8 @@
3737
from .files import FileField, ImageFileField
3838
from starlette.datastructures import UploadFile
3939
from ellar.common.datastructures import UploadFile as EllarUploadFile
40-
40+
from .widget import FieldWidget
41+
from .label import FormLabel
4142

4243
__ZFORM_TYPES__ = {
4344
int: IntegerField,
@@ -126,4 +127,7 @@ def get_field_by_annotation(
126127
"BooleanField",
127128
"FileField",
128129
"ImageFileField",
130+
"FieldBase",
131+
"ChoiceField",
132+
"FieldWidget",
129133
]

0 commit comments

Comments
 (0)