wagtail-block-fields 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .fields import BaseBlockField, ListField, StructField
2
+
3
+ __all__ = ["BaseBlockField", "StructField", "ListField"]
@@ -0,0 +1,269 @@
1
+ """
2
+ StructField and ListField for Wagtail.
3
+
4
+ API based on how StreamField wraps StreamBlock:
5
+ https://github.com/wagtail/wagtail/blob/main/wagtail/fields.py
6
+ https://github.com/wagtail/wagtail/blob/main/wagtail/blocks/stream_block.py
7
+ """
8
+
9
+ import json
10
+
11
+ from django.core.serializers.json import DjangoJSONEncoder
12
+ from django.db import models
13
+ from django.utils.functional import cached_property
14
+
15
+ from wagtail.blocks import Block, BlockField, ListBlock, StructBlock
16
+ from wagtail.blocks.definition_lookup import (
17
+ BlockDefinitionLookup,
18
+ BlockDefinitionLookupBuilder,
19
+ )
20
+ from wagtail.blocks.list_block import ListValue
21
+ from wagtail.blocks.struct_block import StructValue
22
+ from wagtail.fields import Creator
23
+
24
+
25
+ class _ListBlock(ListBlock):
26
+ """Fixes missing form data handling for nested blocks."""
27
+
28
+ def value_from_datadict(self, data, files, prefix):
29
+ if self.value_omitted_from_data(data, files, prefix):
30
+ return self.get_default()
31
+ return super().value_from_datadict(data, files, prefix)
32
+
33
+
34
+ class _StructBlock(StructBlock):
35
+ """Fixes missing form data handling for nested blocks."""
36
+
37
+ def value_from_datadict(self, data, files, prefix):
38
+ return self._to_struct_value(
39
+ [
40
+ (
41
+ name,
42
+ (
43
+ block.value_from_datadict(data, files, f"{prefix}-{name}")
44
+ if not block.value_omitted_from_data(data, files, f"{prefix}-{name}")
45
+ else block.get_default()
46
+ ),
47
+ )
48
+ for name, block in self.child_blocks.items()
49
+ ]
50
+ )
51
+
52
+
53
+ class BaseBlockField(models.Field):
54
+ def __init__(self, block_lookup=None, **kwargs):
55
+ self.block_lookup = block_lookup
56
+ super().__init__(**kwargs)
57
+
58
+ @cached_property
59
+ def block(self):
60
+ raise NotImplementedError
61
+
62
+ def empty_value(self):
63
+ raise NotImplementedError
64
+
65
+ def _get_block_from_arg(self, block_arg):
66
+ has_block_lookup = self.block_lookup is not None
67
+ if has_block_lookup:
68
+ lookup = BlockDefinitionLookup(self.block_lookup)
69
+
70
+ if isinstance(block_arg, Block):
71
+ return block_arg
72
+ elif isinstance(block_arg, int) and has_block_lookup:
73
+ return lookup.get_block(block_arg)
74
+ elif isinstance(block_arg, type) and issubclass(block_arg, Block):
75
+ return block_arg()
76
+ return None
77
+
78
+ @property
79
+ def json_field(self):
80
+ return models.JSONField(encoder=DjangoJSONEncoder)
81
+
82
+ def get_internal_type(self):
83
+ return "JSONField"
84
+
85
+ def get_lookup(self, lookup_name):
86
+ return self.json_field.get_lookup(lookup_name)
87
+
88
+ def get_transform(self, lookup_name):
89
+ return self.json_field.get_transform(lookup_name)
90
+
91
+ def get_db_prep_value(self, value, connection, prepared=False):
92
+ if not prepared:
93
+ value = self.get_prep_value(value)
94
+ return self.json_field.get_db_prep_value(value, connection=connection, prepared=True)
95
+
96
+ def from_db_value(self, value, expression, connection):
97
+ value = self.json_field.from_db_value(value, expression, connection)
98
+ return self.to_python(value)
99
+
100
+ def formfield(self, **kwargs):
101
+ defaults = {"form_class": BlockField, "block": self.block}
102
+ defaults.update(kwargs)
103
+ return super().formfield(**defaults)
104
+
105
+ def value_to_string(self, obj):
106
+ value = self.value_from_object(obj)
107
+ return json.dumps(self.get_prep_value(value), cls=self.json_field.encoder)
108
+
109
+ def get_searchable_content(self, value):
110
+ return self.block.get_searchable_content(value)
111
+
112
+ def extract_references(self, value):
113
+ yield from self.block.extract_references(value)
114
+
115
+ def check(self, **kwargs):
116
+ errors = super().check(**kwargs)
117
+ errors.extend(self.block.check(field=self, **kwargs))
118
+ return errors
119
+
120
+ def contribute_to_class(self, cls, name, **kwargs):
121
+ super().contribute_to_class(cls, name, **kwargs)
122
+ setattr(cls, self.name, Creator(self))
123
+
124
+
125
+ class StructField(BaseBlockField):
126
+ def __init__(self, block_type, block_lookup=None, **kwargs):
127
+ self.block_type_arg = block_type
128
+ super().__init__(block_lookup=block_lookup, **kwargs)
129
+
130
+ @cached_property
131
+ def block(self):
132
+ block = self._get_block_from_arg(self.block_type_arg)
133
+
134
+ if block is None:
135
+ if isinstance(self.block_type_arg, list):
136
+ has_block_lookup = self.block_lookup is not None
137
+ if has_block_lookup:
138
+ lookup = BlockDefinitionLookup(self.block_lookup)
139
+
140
+ child_blocks = []
141
+ for name, child_block in self.block_type_arg:
142
+ if isinstance(child_block, int) and has_block_lookup:
143
+ child_blocks.append((name, lookup.get_block(child_block)))
144
+ else:
145
+ child_blocks.append((name, child_block))
146
+ block = _StructBlock(child_blocks)
147
+ else:
148
+ raise TypeError(
149
+ f"StructField requires a StructBlock instance, class, or list of "
150
+ f"(name, block) tuples, got {type(self.block_type_arg).__name__}"
151
+ )
152
+
153
+ if not isinstance(block, StructBlock):
154
+ raise TypeError(f"StructField requires a StructBlock, got {type(block).__name__}")
155
+
156
+ if not isinstance(block, _StructBlock):
157
+ block = _StructBlock(list(block.child_blocks.items()))
158
+
159
+ return block
160
+
161
+ @property
162
+ def struct_block(self):
163
+ return self.block
164
+
165
+ def empty_value(self):
166
+ return self.block.get_default()
167
+
168
+ def get_default(self):
169
+ return self.block.get_default()
170
+
171
+ def deconstruct(self):
172
+ name, path, _, kwargs = super().deconstruct()
173
+ lookup = BlockDefinitionLookupBuilder()
174
+ block_types = [
175
+ (child_name, lookup.add_block(child_block))
176
+ for child_name, child_block in self.block.child_blocks.items()
177
+ ]
178
+ args = [block_types]
179
+ kwargs["block_lookup"] = lookup.get_lookup_as_dict()
180
+ return name, path, args, kwargs
181
+
182
+ def to_python(self, value):
183
+ if value is None:
184
+ return self.block.get_default()
185
+ if isinstance(value, StructValue):
186
+ return value
187
+ if isinstance(value, str) and value:
188
+ try:
189
+ value = json.loads(value)
190
+ except ValueError:
191
+ return self.block.get_default()
192
+ if isinstance(value, dict):
193
+ return self.block.to_python(value)
194
+ return self.block.get_default()
195
+
196
+ def get_prep_value(self, value):
197
+ if value is None:
198
+ return {}
199
+ if isinstance(value, StructValue):
200
+ return self.block.get_prep_value(value)
201
+ if isinstance(value, dict):
202
+ return value
203
+ return {}
204
+
205
+
206
+ class ListField(BaseBlockField):
207
+ def __init__(self, child_block, block_lookup=None, **kwargs):
208
+ self.block_opts = {}
209
+ for arg in ["min_num", "max_num"]:
210
+ if arg in kwargs:
211
+ self.block_opts[arg] = kwargs.pop(arg)
212
+
213
+ self.child_block_arg = child_block
214
+ super().__init__(block_lookup=block_lookup, **kwargs)
215
+
216
+ @cached_property
217
+ def block(self):
218
+ child_block = self._get_block_from_arg(self.child_block_arg)
219
+
220
+ if child_block is None:
221
+ raise TypeError(
222
+ f"ListField requires a Block instance or class, "
223
+ f"got {type(self.child_block_arg).__name__}"
224
+ )
225
+
226
+ block = _ListBlock(child_block)
227
+ block.set_meta_options(self.block_opts)
228
+ return block
229
+
230
+ @property
231
+ def list_block(self):
232
+ return self.block
233
+
234
+ def empty_value(self):
235
+ return self.block.empty_value()
236
+
237
+ def get_default(self):
238
+ return self.block.empty_value()
239
+
240
+ def deconstruct(self):
241
+ name, path, _, kwargs = super().deconstruct()
242
+ lookup = BlockDefinitionLookupBuilder()
243
+ block_id = lookup.add_block(self.block.child_block)
244
+ args = [block_id]
245
+ kwargs["block_lookup"] = lookup.get_lookup_as_dict()
246
+ return name, path, args, kwargs
247
+
248
+ def to_python(self, value):
249
+ if value is None:
250
+ return self.block.empty_value()
251
+ if isinstance(value, ListValue):
252
+ return value
253
+ if isinstance(value, str) and value:
254
+ try:
255
+ value = json.loads(value)
256
+ except ValueError:
257
+ return self.block.empty_value()
258
+ if isinstance(value, list):
259
+ return self.block.to_python(value)
260
+ return self.block.empty_value()
261
+
262
+ def get_prep_value(self, value):
263
+ if value is None:
264
+ return []
265
+ if isinstance(value, ListValue):
266
+ return self.block.get_prep_value(value)
267
+ if isinstance(value, list):
268
+ return self.block.get_prep_value(self.block.normalize(value))
269
+ return []
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: wagtail-block-fields
3
+ Version: 0.1.0
4
+ Summary: Allows using StructBlock and ListBlock as model fields
5
+ Project-URL: Repository, https://github.com/joeyjurjens/wagtail-block-fields
6
+ Author-email: Joey Jurjens <joeyjurjens@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: wagtail>=6.3
11
+ Description-Content-Type: text/markdown
12
+
13
+ # wagtail-block-fields
14
+
15
+ Use Wagtail's `StructBlock` and `ListBlock` as standalone model fields.
16
+
17
+ Just like `StreamField` wraps `StreamBlock`, this package provides:
18
+ - **`StructField`** - wraps a `StructBlock`
19
+ - **`ListField`** - wraps a `ListBlock`
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install wagtail-block-fields
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### StructField
30
+
31
+ ```python
32
+ from wagtail.blocks import CharBlock, StructBlock
33
+ from wagtail_block_fields import StructField
34
+
35
+ class AddressBlock(StructBlock):
36
+ street = CharBlock()
37
+ city = CharBlock()
38
+ postal_code = CharBlock()
39
+
40
+ class MyPage(Page):
41
+ address = StructField(AddressBlock())
42
+
43
+ contact = StructField([
44
+ ('email', CharBlock()),
45
+ ('phone', CharBlock()),
46
+ ])
47
+ ```
48
+
49
+ ### ListField
50
+
51
+ ```python
52
+ from wagtail.blocks import CharBlock
53
+ from wagtail_block_fields import ListField
54
+
55
+ class MyPage(Page):
56
+ tags = ListField(CharBlock())
57
+ categories = ListField(CharBlock(), min_num=1, max_num=5)
58
+ addresses = ListField(AddressBlock())
59
+ ```
60
+
61
+ ### In templates
62
+
63
+ ```html
64
+ <p>{{ page.address.street }}, {{ page.address.city }}</p>
65
+
66
+ <ul>
67
+ {% for tag in page.tags %}
68
+ <li>{{ tag }}</li>
69
+ {% endfor %}
70
+ </ul>
71
+
72
+ {% for address in page.addresses %}
73
+ <p>{{ address.street }}, {{ address.city }}</p>
74
+ {% endfor %}
75
+ ```
76
+
77
+ ## Why?
78
+
79
+ Sometimes you need structured JSON data in a single field without the complexity of StreamField. These fields:
80
+
81
+ - Store data as JSON in a single database column
82
+ - Provide full Wagtail admin editing UI
83
+ - Support validation, search indexing, and reference extraction
84
+ - Work with migrations just like StreamField
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,6 @@
1
+ wagtail_block_fields/__init__.py,sha256=vePyqYGTvTyRZRgl8-LMEiV_2UcQNMaOOuAsV1uDsbQ,117
2
+ wagtail_block_fields/fields.py,sha256=pzsktMIEsAU-EtcN9ilAvxZvO6QZvBg-EkC3aKoLpiM,9018
3
+ wagtail_block_fields-0.1.0.dist-info/METADATA,sha256=_17MWiWAZi8jzjWqhnk0TdYMstbMy-GfYjKtgnuRumw,1995
4
+ wagtail_block_fields-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ wagtail_block_fields-0.1.0.dist-info/licenses/LICENSE,sha256=6wvTtd0K1ifRwo0UYlFt5ZvqPfO7ir1rip70grR8oj0,1069
6
+ wagtail_block_fields-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joey Jurjens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.