django-structured-metaobjects 1.0.0__py2.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.
- django_structured_metaobjects-1.0.0.dist-info/METADATA +112 -0
- django_structured_metaobjects-1.0.0.dist-info/RECORD +40 -0
- django_structured_metaobjects-1.0.0.dist-info/WHEEL +6 -0
- django_structured_metaobjects-1.0.0.dist-info/licenses/LICENSE +21 -0
- django_structured_metaobjects-1.0.0.dist-info/top_level.txt +2 -0
- structured_metaobjects/__init__.py +14 -0
- structured_metaobjects/admin.py +20 -0
- structured_metaobjects/apps.py +8 -0
- structured_metaobjects/compiler.py +161 -0
- structured_metaobjects/forms.py +36 -0
- structured_metaobjects/migrations/0001_initial.py +73 -0
- structured_metaobjects/migrations/__init__.py +0 -0
- structured_metaobjects/models.py +103 -0
- structured_metaobjects/schema_builder.py +166 -0
- structured_metaobjects/serializers.py +37 -0
- structured_metaobjects/static/structured_metaobjects/admin/meta_instance.js +23 -0
- structured_metaobjects/urls.py +12 -0
- structured_metaobjects/views.py +35 -0
- tests/__init__.py +0 -0
- tests/app/__init__.py +0 -0
- tests/app/app/__init__.py +0 -0
- tests/app/app/asgi.py +5 -0
- tests/app/app/settings.py +77 -0
- tests/app/app/urls.py +14 -0
- tests/app/app/wsgi.py +5 -0
- tests/app/test_module/__init__.py +0 -0
- tests/app/test_module/admin.py +8 -0
- tests/app/test_module/apps.py +8 -0
- tests/app/test_module/migrations/0001_initial.py +22 -0
- tests/app/test_module/migrations/__init__.py +0 -0
- tests/app/test_module/models.py +12 -0
- tests/app/test_module/serializers.py +9 -0
- tests/app/test_module/views.py +9 -0
- tests/test_admin.py +25 -0
- tests/test_compiler.py +462 -0
- tests/test_forms.py +32 -0
- tests/test_models.py +213 -0
- tests/test_schema_builder.py +81 -0
- tests/test_serializers.py +79 -0
- tests/test_views.py +120 -0
tests/test_compiler.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
5
|
+
|
|
6
|
+
from structured_metaobjects.compiler import (
|
|
7
|
+
_cache,
|
|
8
|
+
_field_type,
|
|
9
|
+
_json_schema_extra,
|
|
10
|
+
build_pydantic_model,
|
|
11
|
+
clear_cache,
|
|
12
|
+
get_json_schema,
|
|
13
|
+
)
|
|
14
|
+
from structured_metaobjects.models import MetaType
|
|
15
|
+
from structured_metaobjects.schema_builder import MetaFieldKind, MetaTypeFieldDef
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.django_db
|
|
19
|
+
class TestBuildPydanticModel:
|
|
20
|
+
def _make_meta_type(self, name, schema):
|
|
21
|
+
return MetaType.objects.create(name=name, schema=schema)
|
|
22
|
+
|
|
23
|
+
def test_build_simple_string_field(self):
|
|
24
|
+
mt = self._make_meta_type("simple", [
|
|
25
|
+
{"name": "title", "kind": "string", "required": True},
|
|
26
|
+
])
|
|
27
|
+
model_cls = build_pydantic_model(mt)
|
|
28
|
+
assert issubclass(model_cls, PydanticBaseModel)
|
|
29
|
+
instance = model_cls(title="Hello")
|
|
30
|
+
assert instance.title == "Hello"
|
|
31
|
+
|
|
32
|
+
def test_build_multiple_field_types(self):
|
|
33
|
+
mt = self._make_meta_type("multi", [
|
|
34
|
+
{"name": "name", "kind": "string", "required": True},
|
|
35
|
+
{"name": "age", "kind": "number", "integer": True},
|
|
36
|
+
{"name": "score", "kind": "number"},
|
|
37
|
+
{"name": "active", "kind": "boolean"},
|
|
38
|
+
])
|
|
39
|
+
model_cls = build_pydantic_model(mt)
|
|
40
|
+
instance = model_cls(name="Alice", age=30, score=9.5, active=True)
|
|
41
|
+
assert instance.name == "Alice"
|
|
42
|
+
assert instance.age == 30
|
|
43
|
+
|
|
44
|
+
def test_build_optional_field_defaults_to_none(self):
|
|
45
|
+
mt = self._make_meta_type("opt", [
|
|
46
|
+
{"name": "bio", "kind": "string", "multiline": True, "required": False},
|
|
47
|
+
])
|
|
48
|
+
model_cls = build_pydantic_model(mt)
|
|
49
|
+
instance = model_cls()
|
|
50
|
+
assert instance.bio is None
|
|
51
|
+
|
|
52
|
+
def test_build_group_field(self):
|
|
53
|
+
mt = self._make_meta_type("grp", [
|
|
54
|
+
{
|
|
55
|
+
"name": "address",
|
|
56
|
+
"kind": "group",
|
|
57
|
+
"children": [
|
|
58
|
+
{"name": "street", "kind": "string", "required": True},
|
|
59
|
+
{"name": "city", "kind": "string", "required": True},
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
])
|
|
63
|
+
model_cls = build_pydantic_model(mt)
|
|
64
|
+
instance = model_cls(address={"street": "123 Main", "city": "NYC"})
|
|
65
|
+
assert instance.address.street == "123 Main"
|
|
66
|
+
|
|
67
|
+
def test_build_list_field(self):
|
|
68
|
+
mt = self._make_meta_type("lst", [
|
|
69
|
+
{
|
|
70
|
+
"name": "items",
|
|
71
|
+
"kind": "list",
|
|
72
|
+
"children": [
|
|
73
|
+
{"name": "label", "kind": "string", "required": True},
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
])
|
|
77
|
+
model_cls = build_pydantic_model(mt)
|
|
78
|
+
instance = model_cls(items=[{"label": "A"}, {"label": "B"}])
|
|
79
|
+
assert len(instance.items) == 2
|
|
80
|
+
assert instance.items[0].label == "A"
|
|
81
|
+
|
|
82
|
+
def test_build_translated_field(self):
|
|
83
|
+
mt = self._make_meta_type("trans", [
|
|
84
|
+
{"name": "title", "kind": "string", "required": True, "translated": True},
|
|
85
|
+
])
|
|
86
|
+
model_cls = build_pydantic_model(mt)
|
|
87
|
+
instance = model_cls(title={"en": "Hello", "it": "Ciao"})
|
|
88
|
+
assert instance.title["en"] == "Hello"
|
|
89
|
+
|
|
90
|
+
def test_build_ref_field(self):
|
|
91
|
+
mt = self._make_meta_type("ref", [
|
|
92
|
+
{"name": "page", "kind": "ref", "target_model": "test_module.Page"},
|
|
93
|
+
])
|
|
94
|
+
model_cls = build_pydantic_model(mt)
|
|
95
|
+
assert model_cls is not None
|
|
96
|
+
|
|
97
|
+
def test_build_queryset_field(self):
|
|
98
|
+
mt = self._make_meta_type("qs", [
|
|
99
|
+
{"name": "pages", "kind": "queryset", "target_model": "test_module.Page"},
|
|
100
|
+
])
|
|
101
|
+
model_cls = build_pydantic_model(mt)
|
|
102
|
+
assert model_cls is not None
|
|
103
|
+
|
|
104
|
+
def test_invalid_ref_target_raises(self):
|
|
105
|
+
mt = self._make_meta_type("badref", [
|
|
106
|
+
{"name": "thing", "kind": "ref", "target_model": "invalid"},
|
|
107
|
+
])
|
|
108
|
+
with pytest.raises(ValueError, match="target_model"):
|
|
109
|
+
build_pydantic_model(mt)
|
|
110
|
+
|
|
111
|
+
def test_build_html_field(self):
|
|
112
|
+
mt = self._make_meta_type("html", [
|
|
113
|
+
{"name": "content", "kind": "html", "required": True},
|
|
114
|
+
])
|
|
115
|
+
model_cls = build_pydantic_model(mt)
|
|
116
|
+
instance = model_cls(content="<p>Hello</p>")
|
|
117
|
+
assert instance.content == "<p>Hello</p>"
|
|
118
|
+
|
|
119
|
+
def test_build_date_field(self):
|
|
120
|
+
mt = self._make_meta_type("date", [
|
|
121
|
+
{"name": "birthday", "kind": "date", "required": True},
|
|
122
|
+
])
|
|
123
|
+
model_cls = build_pydantic_model(mt)
|
|
124
|
+
instance = model_cls(birthday=date(2000, 1, 15))
|
|
125
|
+
assert instance.birthday == date(2000, 1, 15)
|
|
126
|
+
|
|
127
|
+
def test_build_date_field_from_string(self):
|
|
128
|
+
mt = self._make_meta_type("date_str", [
|
|
129
|
+
{"name": "published", "kind": "date", "required": True},
|
|
130
|
+
])
|
|
131
|
+
model_cls = build_pydantic_model(mt)
|
|
132
|
+
instance = model_cls(published="2023-05-20")
|
|
133
|
+
assert instance.published == date(2023, 5, 20)
|
|
134
|
+
|
|
135
|
+
def test_build_datetime_field(self):
|
|
136
|
+
mt = self._make_meta_type("datetime", [
|
|
137
|
+
{"name": "created_at", "kind": "datetime", "required": True},
|
|
138
|
+
])
|
|
139
|
+
model_cls = build_pydantic_model(mt)
|
|
140
|
+
dt = datetime(2023, 6, 15, 10, 30, 0)
|
|
141
|
+
instance = model_cls(created_at=dt)
|
|
142
|
+
assert instance.created_at == dt
|
|
143
|
+
|
|
144
|
+
def test_build_datetime_field_from_string(self):
|
|
145
|
+
mt = self._make_meta_type("datetime_str", [
|
|
146
|
+
{"name": "updated_at", "kind": "datetime", "required": True},
|
|
147
|
+
])
|
|
148
|
+
model_cls = build_pydantic_model(mt)
|
|
149
|
+
instance = model_cls(updated_at="2023-06-15T10:30:00")
|
|
150
|
+
assert instance.updated_at == datetime(2023, 6, 15, 10, 30, 0)
|
|
151
|
+
|
|
152
|
+
def test_build_select_field(self):
|
|
153
|
+
mt = self._make_meta_type("select", [
|
|
154
|
+
{
|
|
155
|
+
"name": "status",
|
|
156
|
+
"kind": "select",
|
|
157
|
+
"required": True,
|
|
158
|
+
"choices": [
|
|
159
|
+
{"value": "draft", "label": "Draft"},
|
|
160
|
+
{"value": "published", "label": "Published"},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
])
|
|
164
|
+
model_cls = build_pydantic_model(mt)
|
|
165
|
+
instance = model_cls(status="published")
|
|
166
|
+
assert instance.status == "published"
|
|
167
|
+
|
|
168
|
+
def test_build_select_field_optional(self):
|
|
169
|
+
mt = self._make_meta_type("select_opt", [
|
|
170
|
+
{
|
|
171
|
+
"name": "category",
|
|
172
|
+
"kind": "select",
|
|
173
|
+
"required": False,
|
|
174
|
+
"choices": [
|
|
175
|
+
{"value": "news"},
|
|
176
|
+
{"value": "blog"},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
])
|
|
180
|
+
model_cls = build_pydantic_model(mt)
|
|
181
|
+
instance = model_cls()
|
|
182
|
+
assert instance.category is None
|
|
183
|
+
instance2 = model_cls(category="news")
|
|
184
|
+
assert instance2.category == "news"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.django_db
|
|
188
|
+
class TestCache:
|
|
189
|
+
def test_cache_hit(self):
|
|
190
|
+
mt = MetaType.objects.create(name="cached", schema=[
|
|
191
|
+
{"name": "x", "kind": "string"},
|
|
192
|
+
])
|
|
193
|
+
clear_cache()
|
|
194
|
+
model1 = build_pydantic_model(mt)
|
|
195
|
+
model2 = build_pydantic_model(mt)
|
|
196
|
+
assert model1 is model2
|
|
197
|
+
|
|
198
|
+
def test_clear_cache_by_id(self):
|
|
199
|
+
mt = MetaType.objects.create(name="cc", schema=[
|
|
200
|
+
{"name": "x", "kind": "string"},
|
|
201
|
+
])
|
|
202
|
+
build_pydantic_model(mt)
|
|
203
|
+
assert any(k[0] == mt.pk for k in _cache)
|
|
204
|
+
clear_cache(mt.pk)
|
|
205
|
+
assert not any(k[0] == mt.pk for k in _cache)
|
|
206
|
+
|
|
207
|
+
def test_clear_cache_all(self):
|
|
208
|
+
mt = MetaType.objects.create(name="ca", schema=[
|
|
209
|
+
{"name": "x", "kind": "string"},
|
|
210
|
+
])
|
|
211
|
+
build_pydantic_model(mt)
|
|
212
|
+
clear_cache()
|
|
213
|
+
assert len(_cache) == 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestFieldType:
|
|
217
|
+
"""Unit tests for _field_type helper."""
|
|
218
|
+
|
|
219
|
+
def test_string_returns_str(self):
|
|
220
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.string)
|
|
221
|
+
assert _field_type(fdef, "Test") is str
|
|
222
|
+
|
|
223
|
+
def test_html_returns_str(self):
|
|
224
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.html)
|
|
225
|
+
assert _field_type(fdef, "Test") is str
|
|
226
|
+
|
|
227
|
+
def test_number_returns_float(self):
|
|
228
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.number)
|
|
229
|
+
assert _field_type(fdef, "Test") is float
|
|
230
|
+
|
|
231
|
+
def test_number_with_integer_returns_int(self):
|
|
232
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.number, integer=True)
|
|
233
|
+
assert _field_type(fdef, "Test") is int
|
|
234
|
+
|
|
235
|
+
def test_boolean_returns_bool(self):
|
|
236
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.boolean)
|
|
237
|
+
assert _field_type(fdef, "Test") is bool
|
|
238
|
+
|
|
239
|
+
def test_date_returns_date(self):
|
|
240
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.date)
|
|
241
|
+
assert _field_type(fdef, "Test") is date
|
|
242
|
+
|
|
243
|
+
def test_datetime_returns_datetime(self):
|
|
244
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.datetime)
|
|
245
|
+
assert _field_type(fdef, "Test") is datetime
|
|
246
|
+
|
|
247
|
+
def test_select_returns_str(self):
|
|
248
|
+
fdef = MetaTypeFieldDef(
|
|
249
|
+
name="x",
|
|
250
|
+
kind=MetaFieldKind.select,
|
|
251
|
+
choices=[{"value": "a"}, {"value": "b"}],
|
|
252
|
+
)
|
|
253
|
+
assert _field_type(fdef, "Test") is str
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestJsonSchemaExtra:
|
|
257
|
+
"""Unit tests for _json_schema_extra helper."""
|
|
258
|
+
|
|
259
|
+
def test_string_multiline_format(self):
|
|
260
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.string, multiline=True)
|
|
261
|
+
extra = _json_schema_extra(fdef)
|
|
262
|
+
assert extra["format"] == "textarea"
|
|
263
|
+
|
|
264
|
+
def test_html_format(self):
|
|
265
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.html)
|
|
266
|
+
extra = _json_schema_extra(fdef)
|
|
267
|
+
assert extra["format"] == "html"
|
|
268
|
+
|
|
269
|
+
def test_select_choices(self):
|
|
270
|
+
fdef = MetaTypeFieldDef(
|
|
271
|
+
name="x",
|
|
272
|
+
kind=MetaFieldKind.select,
|
|
273
|
+
choices=[
|
|
274
|
+
{"value": "draft", "label": "Draft"},
|
|
275
|
+
{"value": "published", "label": "Published"},
|
|
276
|
+
],
|
|
277
|
+
)
|
|
278
|
+
extra = _json_schema_extra(fdef)
|
|
279
|
+
assert "oneOf" in extra
|
|
280
|
+
assert len(extra["oneOf"]) == 2
|
|
281
|
+
assert extra["oneOf"][0] == {"const": "draft", "title": "Draft"}
|
|
282
|
+
assert extra["oneOf"][1] == {"const": "published", "title": "Published"}
|
|
283
|
+
|
|
284
|
+
def test_string_length_constraints(self):
|
|
285
|
+
fdef = MetaTypeFieldDef(
|
|
286
|
+
name="x", kind=MetaFieldKind.string, min_length=5, max_length=100
|
|
287
|
+
)
|
|
288
|
+
extra = _json_schema_extra(fdef)
|
|
289
|
+
assert extra["minLength"] == 5
|
|
290
|
+
assert extra["maxLength"] == 100
|
|
291
|
+
|
|
292
|
+
def test_string_placeholder(self):
|
|
293
|
+
fdef = MetaTypeFieldDef(
|
|
294
|
+
name="x", kind=MetaFieldKind.string, placeholder="Enter text"
|
|
295
|
+
)
|
|
296
|
+
extra = _json_schema_extra(fdef)
|
|
297
|
+
assert extra["placeholder"] == "Enter text"
|
|
298
|
+
|
|
299
|
+
def test_html_placeholder(self):
|
|
300
|
+
fdef = MetaTypeFieldDef(
|
|
301
|
+
name="x", kind=MetaFieldKind.html, placeholder="Enter HTML"
|
|
302
|
+
)
|
|
303
|
+
extra = _json_schema_extra(fdef)
|
|
304
|
+
assert extra["placeholder"] == "Enter HTML"
|
|
305
|
+
|
|
306
|
+
def test_number_constraints(self):
|
|
307
|
+
fdef = MetaTypeFieldDef(
|
|
308
|
+
name="x", kind=MetaFieldKind.number, minimum=0, maximum=100
|
|
309
|
+
)
|
|
310
|
+
extra = _json_schema_extra(fdef)
|
|
311
|
+
assert extra["minimum"] == 0
|
|
312
|
+
assert extra["maximum"] == 100
|
|
313
|
+
|
|
314
|
+
def test_no_extra_for_plain_boolean(self):
|
|
315
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.boolean)
|
|
316
|
+
extra = _json_schema_extra(fdef)
|
|
317
|
+
assert extra == {}
|
|
318
|
+
|
|
319
|
+
def test_no_extra_for_plain_date(self):
|
|
320
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.date)
|
|
321
|
+
extra = _json_schema_extra(fdef)
|
|
322
|
+
assert extra == {}
|
|
323
|
+
|
|
324
|
+
def test_no_extra_for_plain_datetime(self):
|
|
325
|
+
fdef = MetaTypeFieldDef(name="x", kind=MetaFieldKind.datetime)
|
|
326
|
+
extra = _json_schema_extra(fdef)
|
|
327
|
+
assert extra == {}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@pytest.mark.django_db
|
|
331
|
+
class TestGetJsonSchema:
|
|
332
|
+
def test_returns_dict(self):
|
|
333
|
+
mt = MetaType.objects.create(name="js", schema=[
|
|
334
|
+
{"name": "title", "kind": "string", "required": True},
|
|
335
|
+
])
|
|
336
|
+
schema = get_json_schema(mt)
|
|
337
|
+
assert isinstance(schema, dict)
|
|
338
|
+
assert "properties" in schema
|
|
339
|
+
assert "title" in schema["properties"]
|
|
340
|
+
|
|
341
|
+
def test_schema_includes_date_format(self):
|
|
342
|
+
mt = MetaType.objects.create(name="js_date", schema=[
|
|
343
|
+
{"name": "published", "kind": "date", "required": True},
|
|
344
|
+
])
|
|
345
|
+
schema = get_json_schema(mt)
|
|
346
|
+
assert "published" in schema["properties"]
|
|
347
|
+
assert schema["properties"]["published"]["type"] == "string"
|
|
348
|
+
assert schema["properties"]["published"]["format"] == "date"
|
|
349
|
+
|
|
350
|
+
def test_schema_includes_datetime_format(self):
|
|
351
|
+
mt = MetaType.objects.create(name="js_dt", schema=[
|
|
352
|
+
{"name": "created", "kind": "datetime", "required": True},
|
|
353
|
+
])
|
|
354
|
+
schema = get_json_schema(mt)
|
|
355
|
+
assert "created" in schema["properties"]
|
|
356
|
+
assert schema["properties"]["created"]["type"] == "string"
|
|
357
|
+
assert schema["properties"]["created"]["format"] == "date-time"
|
|
358
|
+
|
|
359
|
+
def test_schema_includes_html_format(self):
|
|
360
|
+
mt = MetaType.objects.create(name="js_html", schema=[
|
|
361
|
+
{"name": "body", "kind": "html", "required": True},
|
|
362
|
+
])
|
|
363
|
+
schema = get_json_schema(mt)
|
|
364
|
+
assert "body" in schema["properties"]
|
|
365
|
+
assert schema["properties"]["body"].get("format") == "html"
|
|
366
|
+
|
|
367
|
+
def test_schema_includes_select_choices(self):
|
|
368
|
+
mt = MetaType.objects.create(name="js_sel", schema=[
|
|
369
|
+
{
|
|
370
|
+
"name": "status",
|
|
371
|
+
"kind": "select",
|
|
372
|
+
"required": True,
|
|
373
|
+
"choices": [
|
|
374
|
+
{"value": "draft", "label": "Draft"},
|
|
375
|
+
{"value": "live", "label": "Live"},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
])
|
|
379
|
+
schema = get_json_schema(mt)
|
|
380
|
+
assert "status" in schema["properties"]
|
|
381
|
+
prop = schema["properties"]["status"]
|
|
382
|
+
assert "oneOf" in prop
|
|
383
|
+
assert {"const": "draft", "title": "Draft"} in prop["oneOf"]
|
|
384
|
+
assert {"const": "live", "title": "Live"} in prop["oneOf"]
|
|
385
|
+
|
|
386
|
+
def test_schema_includes_string_constraints(self):
|
|
387
|
+
mt = MetaType.objects.create(name="js_str", schema=[
|
|
388
|
+
{
|
|
389
|
+
"name": "title",
|
|
390
|
+
"kind": "string",
|
|
391
|
+
"required": True,
|
|
392
|
+
"min_length": 1,
|
|
393
|
+
"max_length": 255,
|
|
394
|
+
},
|
|
395
|
+
])
|
|
396
|
+
schema = get_json_schema(mt)
|
|
397
|
+
prop = schema["properties"]["title"]
|
|
398
|
+
assert prop.get("minLength") == 1
|
|
399
|
+
assert prop.get("maxLength") == 255
|
|
400
|
+
|
|
401
|
+
def test_schema_includes_number_constraints(self):
|
|
402
|
+
mt = MetaType.objects.create(name="js_num", schema=[
|
|
403
|
+
{
|
|
404
|
+
"name": "rating",
|
|
405
|
+
"kind": "number",
|
|
406
|
+
"required": True,
|
|
407
|
+
"minimum": 0,
|
|
408
|
+
"maximum": 5,
|
|
409
|
+
},
|
|
410
|
+
])
|
|
411
|
+
schema = get_json_schema(mt)
|
|
412
|
+
prop = schema["properties"]["rating"]
|
|
413
|
+
assert prop.get("minimum") == 0
|
|
414
|
+
assert prop.get("maximum") == 5
|
|
415
|
+
|
|
416
|
+
def test_schema_nested_group_with_date(self):
|
|
417
|
+
mt = MetaType.objects.create(name="js_ng", schema=[
|
|
418
|
+
{
|
|
419
|
+
"name": "event",
|
|
420
|
+
"kind": "group",
|
|
421
|
+
"children": [
|
|
422
|
+
{"name": "title", "kind": "string", "required": True},
|
|
423
|
+
{"name": "start_date", "kind": "date", "required": True},
|
|
424
|
+
{"name": "start_time", "kind": "datetime"},
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
])
|
|
428
|
+
schema = get_json_schema(mt)
|
|
429
|
+
assert "event" in schema["properties"]
|
|
430
|
+
|
|
431
|
+
def test_schema_list_with_select(self):
|
|
432
|
+
mt = MetaType.objects.create(name="js_ls", schema=[
|
|
433
|
+
{
|
|
434
|
+
"name": "tags",
|
|
435
|
+
"kind": "list",
|
|
436
|
+
"children": [
|
|
437
|
+
{
|
|
438
|
+
"name": "category",
|
|
439
|
+
"kind": "select",
|
|
440
|
+
"required": True,
|
|
441
|
+
"choices": [
|
|
442
|
+
{"value": "tech"},
|
|
443
|
+
{"value": "news"},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
{"name": "label", "kind": "string", "required": True},
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
])
|
|
450
|
+
schema = get_json_schema(mt)
|
|
451
|
+
assert "tags" in schema["properties"]
|
|
452
|
+
# List fields produce an array schema - may be wrapped in anyOf for optional
|
|
453
|
+
tags_prop = schema["properties"]["tags"]
|
|
454
|
+
# Check for anyOf (optional list) or direct items
|
|
455
|
+
if "anyOf" in tags_prop:
|
|
456
|
+
array_branch = next(
|
|
457
|
+
(b for b in tags_prop["anyOf"] if b.get("type") == "array"), None
|
|
458
|
+
)
|
|
459
|
+
assert array_branch is not None
|
|
460
|
+
assert "items" in array_branch
|
|
461
|
+
else:
|
|
462
|
+
assert "items" in tags_prop or "$ref" in tags_prop
|
tests/test_forms.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from structured.widget.fields import StructuredJSONFormField
|
|
3
|
+
|
|
4
|
+
from structured_metaobjects.forms import MetaInstanceForm
|
|
5
|
+
from structured_metaobjects.models import MetaInstance, MetaType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.django_db
|
|
9
|
+
class TestMetaInstanceForm:
|
|
10
|
+
def _make_type(self):
|
|
11
|
+
return MetaType.objects.create(
|
|
12
|
+
name="Form Type",
|
|
13
|
+
schema=[{"name": "title", "kind": "string", "required": True}],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def test_form_with_existing_instance(self):
|
|
17
|
+
mt = self._make_type()
|
|
18
|
+
mi = MetaInstance.objects.create(
|
|
19
|
+
meta_type=mt,
|
|
20
|
+
data={"title": "Existing"},
|
|
21
|
+
)
|
|
22
|
+
form = MetaInstanceForm(instance=mi)
|
|
23
|
+
assert isinstance(form.fields["data"], StructuredJSONFormField)
|
|
24
|
+
|
|
25
|
+
def test_form_without_meta_type_shows_help_text(self):
|
|
26
|
+
form = MetaInstanceForm()
|
|
27
|
+
assert "Select a meta type" in form.fields["data"].help_text
|
|
28
|
+
|
|
29
|
+
def test_form_with_initial_meta_type(self):
|
|
30
|
+
mt = self._make_type()
|
|
31
|
+
form = MetaInstanceForm(initial={"meta_type": mt.pk})
|
|
32
|
+
assert isinstance(form.fields["data"], StructuredJSONFormField)
|