Skip to content

Commit aae1352

Browse files
boxedjlubcke
andcommitted
Fix call_target__attribute handling
Co-authored-by: Johan Lübcke <johan@lubcke.se>
1 parent c737e97 commit aae1352

13 files changed

Lines changed: 187 additions & 54 deletions

docs/test_doc_cookbook_queries.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ def test_how_do_I_set_the_name_for_a_filter(big_discography):
118118
)
119119

120120
# @test
121+
t = track_table.bind(request=req('get', **{'-query/query': 'artist="black sabbath"'}))
122+
assert t.query.get_advanced_query_param() == '-query/query'
123+
assert t.query.filters.album_artist_name.attr == 'album__artist__name'
124+
assert t.query.filters.album_artist_name.query_name == 'artist'
125+
assert not t.query.form.get_errors(), t.query.form.get_errors()
126+
assert repr(t.query.get_q()) == repr(Q(album__artist__name__iexact='black sabbath'))
127+
assert {row.album.artist.name for row in t.rows} == {'Black Sabbath'}, [row.album.artist.name for row in t.rows]
128+
121129
show_output(track_table, '?-query%2Fquery=artist%3D"black+sabbath"')
122130
# @end
123131

iommi/declarative/namespace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def __call__(self, *args, **kwargs):
105105

106106
if isinstance(call_target, Namespace):
107107
if 'call_target' in call_target:
108+
# TODO: HOLY CRAP DELETE
108109
# Override of the default
109110
call_target.pop('attribute', None)
110111
call_target.pop('cls', None)

iommi/edit_table.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
bind_member,
5555
bind_members,
5656
refine_done_members,
57+
reify_conf,
5758
)
5859
from iommi.refinable import (
5960
Refinable,
@@ -178,12 +179,17 @@ class EditColumn(Column):
178179

179180
field: Field = Refinable()
180181

182+
@classmethod
183+
@dispatch
184+
def from_model(cls, model=None, model_field_name=None, model_field=None, **kwargs):
185+
return reify_conf(cls._from_model(model=model, model_field_name=model_field_name, model_field=model_field, **kwargs))
186+
181187
@classmethod
182188
@dispatch(
183189
filter__call_target__attribute='from_model',
184190
bulk__call_target__attribute='from_model',
185191
)
186-
def from_model(cls, model=None, model_field_name=None, model_field=None, **kwargs):
192+
def _from_model(cls, model=None, model_field_name=None, model_field=None, **kwargs):
187193
return member_from_model(
188194
cls=cls,
189195
model=model,

iommi/form.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
bind_member,
112112
bind_members,
113113
refine_done_members,
114+
reify_conf,
114115
)
115116
from iommi.page import (
116117
Page,
@@ -1162,7 +1163,13 @@ def grouped_choice_tuples(self):
11621163
return groups
11631164

11641165
@classmethod
1166+
@dispatch
11651167
def from_model(cls, model=None, model_field_name=None, model_field=None, **kwargs):
1168+
return reify_conf(cls._from_model(model=model, model_field_name=model_field_name, model_field=model_field, **kwargs))
1169+
1170+
@classmethod
1171+
@dispatch
1172+
def _from_model(cls, model=None, model_field_name=None, model_field=None, **kwargs):
11661173
return member_from_model(
11671174
cls=cls,
11681175
model=model,
@@ -1499,6 +1506,7 @@ def many_to_many_reverse(cls, model_field, **kwargs):
14991506
return cls.many_to_many(model_field=model_field, **kwargs)
15001507

15011508
@classmethod
1509+
@with_defaults
15021510
def hardcoded(cls, **kwargs):
15031511
assert (
15041512
'parsed_data' in kwargs
@@ -1764,13 +1772,19 @@ def on_refine_done(self):
17641772
extra_action_defaults = Namespace()
17651773
crud_type = self.extra.get('crud_type')
17661774
if 'title' not in self.iommi_namespace and crud_type is not None:
1767-
self.title = lambda form, **_: capitalize(
1768-
gettext_lazy('%(crud_type)s %(model_name)s')
1769-
% dict(
1770-
crud_type=gettext_lazy(form.extra.crud_type),
1771-
model_name=(form.model or form.instance)._meta.verbose_name,
1775+
def default_title(form, **_):
1776+
model = (form.model or form.instance)
1777+
if model is None:
1778+
return capitalize(gettext_lazy(form.extra.crud_type))
1779+
1780+
return capitalize(
1781+
gettext_lazy('%(crud_type)s %(model_name)s')
1782+
% dict(
1783+
crud_type=gettext_lazy(form.extra.crud_type),
1784+
model_name=model._meta.verbose_name,
1785+
)
17721786
)
1773-
)
1787+
self.title = default_title
17741788
extra_action_defaults = setdefaults_path(
17751789
extra_action_defaults,
17761790
submit__display_name=lambda form, **_: gettext_lazy('Save') if form.extra.crud_type == 'edit' else capitalize(gettext_lazy(form.extra.crud_type)),

iommi/form__tests.py

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
from iommi.from_model import (
8989
member_from_model,
9090
)
91+
from iommi.member import reify_conf
9192
from iommi.page import (
9293
Page,
9394
)
@@ -1061,12 +1062,12 @@ def test_choice_not_required():
10611062
class MyForm(Form):
10621063
foo = Field.choice(required=False, choices=['bar'])
10631064

1064-
assert do_post(MyForm()).fields.foo.value is None
1065-
assert do_post(MyForm(), foo='bar').fields.foo.value == 'bar'
1066-
form = do_post(MyForm(), do_post_key_validation=False, baz='bar')
1065+
assert do_post(MyForm.create()).fields.foo.value is None
1066+
assert do_post(MyForm.create(), foo='bar').fields.foo.value == 'bar'
1067+
form = do_post(MyForm.create(), do_post_key_validation=False, baz='bar')
10671068
assert form.fields.foo.value is None
10681069

1069-
form = do_post(MyForm(), foo='baz')
1070+
form = do_post(MyForm.create(), foo='baz')
10701071
assert form.fields.foo.value is None
10711072
assert form.get_errors() == {'fields': {'foo': {'baz not in available choices'}}}
10721073

@@ -1400,6 +1401,10 @@ class FooModel(Model):
14001401
assert not Field.from_model(FooModel, 'c').refine_done().required
14011402
assert Field.from_model(FooModel, 'd').refine_done().required
14021403

1404+
# Conf overrides default
1405+
assert Field.from_model(FooModel, 'a', required=True).refine_done().required
1406+
assert not Field.from_model(FooModel, 'd', required=False).refine_done().required
1407+
14031408

14041409
@pytest.mark.django
14051410
@pytest.mark.filterwarnings("ignore:Model 'tests.foomodel' was already registered")
@@ -1549,13 +1554,15 @@ def test_overriding_parse_empty_string_as_none_in_shortcut():
15491554
parse_empty_string_as_none='foo',
15501555
)
15511556
# test overriding parse_empty_string_as_none
1552-
x = member_from_model(
1553-
cls=Field,
1554-
model=Foo,
1555-
model_field=CharField(blank=True),
1556-
factory_lookup={CharField: s},
1557-
factory_lookup_register_function=register_field_factory,
1558-
defaults_factory=field_defaults_factory,
1557+
x = reify_conf(
1558+
member_from_model(
1559+
cls=Field,
1560+
model=Foo,
1561+
model_field=CharField(blank=True),
1562+
factory_lookup={CharField: s},
1563+
factory_lookup_register_function=register_field_factory,
1564+
defaults_factory=field_defaults_factory,
1565+
)
15591566
).refine_done()
15601567

15611568
assert 'foo' == x.parse_empty_string_as_none
@@ -1643,9 +1650,23 @@ def test_field_from_model_many_to_one_foreign_key():
16431650
def test_register_field_factory():
16441651
from tests.models import FooField, RegisterFieldFactoryTest
16451652

1653+
f = Field()
1654+
1655+
register_field_factory(FooField, factory=lambda **kwargs: f)
1656+
1657+
assert Field.from_model(RegisterFieldFactoryTest, 'foo') is f
1658+
1659+
1660+
@pytest.mark.django
1661+
def test_register_field_factory_disallow_non_dict_non_field():
1662+
from tests.models import FooField, RegisterFieldFactoryTest
1663+
16461664
register_field_factory(FooField, factory=lambda **kwargs: 7)
16471665

1648-
assert Field.from_model(RegisterFieldFactoryTest, 'foo') == 7
1666+
with pytest.raises(AssertionError) as e:
1667+
assert Field.from_model(RegisterFieldFactoryTest, 'foo')
1668+
1669+
assert str(e.value) == 'Factories must return a configuration dict or an instance of Field. Got int: "7"'
16491670

16501671

16511672
def shortcut_test(shortcut, raw_and_parsed_data_tuples, normalizing=None, is_list=False):
@@ -2670,7 +2691,7 @@ class Meta:
26702691
def test_shortcut_to_subclass():
26712692
class MyField(Field):
26722693
@classmethod
2673-
@with_defaults()
2694+
@with_defaults
26742695
def my_shortcut(cls, **kwargs):
26752696
return cls(**kwargs) # pragma: no cover: we aren't testing that this shortcut is implemented correctly
26762697

@@ -3526,12 +3547,14 @@ def test_initial_is_set_to_default_of_model():
35263547

35273548

35283549
def test_shoot_config_into_auto_dunder_field():
3529-
Form(
3550+
other_display_name = 'something else'
3551+
form = Form(
35303552
auto__model=FieldFromModelOneToOneTest,
35313553
# attr `foo_one_to_one__foo` creates a field named `foo_one_to_one_foo`. Note that the `__` is collapsed to one `_`!
35323554
auto__include=['foo_one_to_one__foo'],
3533-
fields__foo_one_to_one_foo__display_name='bar',
3555+
fields__foo_one_to_one_foo__display_name=other_display_name,
35343556
).bind(request=req('get'))
3557+
assert form.fields.foo_one_to_one_foo.display_name == other_display_name
35353558

35363559

35373560
@pytest.mark.django_db
@@ -3747,7 +3770,7 @@ def test_action_callbacks_should_be_lazy():
37473770
def test_form_template_override_bug():
37483771
class MyForm(Form):
37493772
@classmethod
3750-
@with_defaults()
3773+
@with_defaults
37513774
def case1(cls, **kwargs):
37523775
kwargs['template'] = 'case1'
37533776
return cls(**kwargs)
@@ -4090,3 +4113,42 @@ def test_parsed_data_does_not_crash_on_non_editable():
40904113
parsed_data=lambda **_: 1,
40914114
)
40924115
).bind(request=req('POST', **{'-submit': ''}))
4116+
4117+
4118+
def test_call_target__attribute():
4119+
class FooForm(Form):
4120+
class Meta:
4121+
auto__model = Foo
4122+
auto__include = ['foo']
4123+
fields__foo__call_target__attribute = 'hardcoded'
4124+
fields__foo__parsed_data = 123
4125+
4126+
form = FooForm().bind(request=req('post', foo='456'))
4127+
assert form.fields.foo.value == 123
4128+
4129+
4130+
def test_call_target__attribute_override_to_radio_button():
4131+
class FooForm(Form):
4132+
class Meta:
4133+
auto__model = ChoicesModel
4134+
fields__color__call_target__attribute = 'radio'
4135+
4136+
form = FooForm().bind(request=req('get'))
4137+
assert form.fields.color.iommi_shortcut_stack == [
4138+
'radio',
4139+
'choice',
4140+
]
4141+
4142+
4143+
def test_shoot_config_into_related_field():
4144+
class FooForm(Form):
4145+
class Meta:
4146+
auto__model = Bar
4147+
auto__include = ['foo__foo']
4148+
fields__foo_foo__call_target__attribute = 'hardcoded'
4149+
fields__foo_foo__parsed_data = 123
4150+
4151+
form = do_post(FooForm.create(), do_post_key_validation=False, foo='456')
4152+
assert form.fields.foo_foo.iommi_shortcut_stack == ['hardcoded']
4153+
assert form.fields.foo_foo.parsed_data == 123
4154+
assert form.fields.foo_foo.value == 123

iommi/from_model.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
Namespace,
2525
setdefaults_path,
2626
)
27-
from iommi.evaluate import evaluate
27+
from iommi.evaluate import (
28+
evaluate,
29+
evaluate_strict,
30+
)
2831
from iommi.refinable import (
2932
Refinable,
3033
RefinableObject,
@@ -52,27 +55,23 @@ def create_members_from_model(
5255
if exclude is not None and model_field_name in exclude:
5356
continue
5457
name = model_field_name.replace('__', '_')
55-
definition = Namespace(
58+
conf = Namespace(
5659
_name=name,
57-
call_target__cls=member_class,
58-
call_target__attribute='from_model',
5960
model_field_name=model_field_name,
6061
model=model,
6162
)
6263
if default_included is False:
6364
setdefaults_path(
64-
definition,
65+
conf,
6566
include=False,
6667
)
6768
if include is not None and name in include:
6869
setdefaults_path(
69-
definition,
70+
conf,
7071
include=True,
7172
)
7273

73-
member = definition()
74-
if member is not None:
75-
members[name] = member
74+
members[name] = member_class._from_model(**conf)
7675

7776
return members
7877

@@ -107,7 +106,7 @@ def member_from_model(
107106
model_field_name=field_path_rest,
108107
**kwargs,
109108
)
110-
result = result.refine(attr=model_field_name)
109+
result.attr = model_field_name
111110
return result
112111
else:
113112
if model is None:
@@ -144,7 +143,17 @@ def no_factory_defined(**_):
144143
return None
145144

146145
# Not strict evaluate on purpose
147-
factory = evaluate(factory, __match_empty=False, model_field=model_field, model_field_name=model_field_name)
146+
if isinstance(factory, dict):
147+
conf_or_instance = factory
148+
else:
149+
conf_or_instance = evaluate_strict(factory, model_field=model_field, model_field_name=model_field_name)
150+
151+
if isinstance(conf_or_instance, cls):
152+
return conf_or_instance
153+
154+
assert isinstance(conf_or_instance, dict), f'Factories must return a configuration dict or an instance of {cls.__name__}. Got {type(conf_or_instance).__name__}: "{conf_or_instance}"'
155+
conf = conf_or_instance
156+
del conf_or_instance
148157

149158
setdefaults_path(
150159
kwargs,
@@ -153,16 +162,16 @@ def no_factory_defined(**_):
153162
)
154163

155164
defaults = defaults_factory(model_field)
156-
if isinstance(factory, Namespace):
157-
factory = setdefaults_path(
158-
Namespace(),
159-
factory,
160-
defaults,
161-
)
162-
else:
163-
kwargs.update(**defaults)
164165

165-
return factory(model_field=model_field, model_field_name=model_field_name, model=model, **kwargs)
166+
return setdefaults_path(
167+
Namespace(),
168+
kwargs,
169+
conf,
170+
defaults,
171+
model_field=model_field,
172+
model_field_name=model_field_name,
173+
model=model,
174+
)
166175

167176

168177
def get_field_by_name(model: Type[Model]) -> Dict[str, DjangoField]:

0 commit comments

Comments
 (0)