Skip to content

Commit d452613

Browse files
authored
Merge pull request #510 from MongoEngine/dict_field
New model form generator: Support of DictField
2 parents e011ceb + 289680a commit d452613

File tree

14 files changed

+421
-117
lines changed

14 files changed

+421
-117
lines changed

docs/api/wtf.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ flask_mongoengine.wtf.fields module
77
-----------------------------------
88

99
.. automodule:: flask_mongoengine.wtf.fields
10+
:special-members: __init__
1011

1112
flask_mongoengine.wtf.models module
1213
-----------------------------------

docs/forms.md

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class BooleanDemoModel(db.Document):
147147

148148
Such definition will create document field, even if nothing selected. The value will
149149
be `None`. If, during edit, `yes` or `no` dropdown values replaced to `---`, then
150-
saved value in document will be aslo changed to `None`.
150+
saved value in document will be also changed to `None`.
151151

152152
By default, `None` value represented as `---` text in dropdown.
153153

@@ -407,7 +407,162 @@ class NumbersDemoModel(db.Document):
407407

408408
## DictField
409409

410-
Not yet documented. Please help us with new pull request.
410+
- API: {class}`.db_fields.DictField`
411+
- Default form field class: {class}`~.MongoDictField`
412+
413+
DictField has `Object` type in terms of Mongo database itself, so basically it defines
414+
document inside document, but without pre-defined structure. That's why this is one
415+
of fields, that has default value specified inside Mongoengine itself, and that's
416+
why is always (almost) created.
417+
418+
The developer should understand that database keyword argument {attr}`default` is
419+
forwarded to form by default, but can be separately overwritten in form. This brings
420+
a lot of options for form field configuration.
421+
422+
Also, should be additionally noted that database `Null` value in form is represented as
423+
empty string. Non-existing field is represented with form {attr}`default` for new
424+
forms (without instance inside) or with empty string for non-empty forms.
425+
426+
Complicated? Probably. That's why this field was completely rewritten in version
427+
**2.0.0**. Check examples, and everything will be clear.
428+
429+
### Form generation behaviour
430+
431+
Our default form generation follow Mongoengine internals and will use database field
432+
default (empty dict) to populate to new form or to not filled field in existing form.
433+
434+
In the same time, we are allowing extending of this behaviour, and not creating
435+
field in database, if default value provided as `None`. In this case, generated
436+
field for new form will be empty, without any pre-filled value.
437+
438+
Same empty field will be displayed in case, when both {attr}`default=None` and
439+
{attr}`null=True` selected, during database form initialization. In this case form
440+
field will be empty, without any placeholder, but on save `null` object will be
441+
created in document.
442+
443+
Also, we even support separated defaults for form field and database field, allowing
444+
any form+database behaviour.
445+
446+
### Examples
447+
448+
#### DictField with default empty dict value
449+
450+
Will place `{}` to form for existing/new fields. This value is hardcodded in parent
451+
MongoEngine project.
452+
453+
```python
454+
"""dict_demo.py"""
455+
from example_app.models import db
456+
457+
458+
class DictDemoModel(db.Document):
459+
"""Documentation example model."""
460+
461+
dict_field = db.DictField()
462+
```
463+
464+
#### DictField with default `None` value, ignored by database
465+
466+
Reminder: Such field is empty in form, and will not create anything in database if
467+
not filled.
468+
469+
```python
470+
"""dict_demo.py"""
471+
from example_app.models import db
472+
473+
474+
class DictDemoModel(db.Document):
475+
"""Documentation example model."""
476+
477+
no_dict_field = db.DictField(default=None)
478+
```
479+
480+
#### DictField with default `None` value, saved to database
481+
482+
Reminder: Such field is empty in form, and will create `null` object in database if
483+
not filled.
484+
485+
```python
486+
"""dict_demo.py"""
487+
from example_app.models import db
488+
489+
490+
class DictDemoModel(db.Document):
491+
"""Documentation example model."""
492+
493+
null_dict_field = db.DictField(default=None, null=True)
494+
```
495+
496+
#### DictField with pre-defined default dict
497+
498+
This value is pre-defined on database level. So behaviour of form and in-code
499+
creation of such objects will be the same - default dict will be saved to database,
500+
if nothing provided to form/instance. Form will be pre-filled with default dict.
501+
502+
```python
503+
"""dict_demo.py"""
504+
from example_app.models import db
505+
506+
507+
def get_default_dict():
508+
"""Example of default dict specification."""
509+
return {"alpha": 1, "text": "text", "float": 1.2}
510+
511+
512+
class DictDemoModel(db.Document):
513+
"""Documentation example model."""
514+
515+
dict_default = db.DictField(default=get_default_dict)
516+
```
517+
518+
#### DictField with pre-defined value and no document object creation on `Null`
519+
520+
This is a case when you do not want to create any record in database document, if
521+
user completely delete pre-filled value in new document form. Here we use different
522+
`null` and `default` values in form field generation and during database object
523+
generation.
524+
525+
```python
526+
"""dict_demo.py"""
527+
from example_app.models import db
528+
529+
530+
def get_default_dict():
531+
"""Example of default dict specification."""
532+
return {"alpha": 1, "text": "text", "float": 1.2}
533+
534+
535+
class DictDemoModel(db.Document):
536+
"""Documentation example model."""
537+
538+
no_dict_prefilled = db.DictField(
539+
default=None,
540+
null=False,
541+
wtf_options={"default": get_default_dict, "null": True},
542+
)
543+
```
544+
545+
#### DictField with pre-defined default dict with `Null` fallback
546+
547+
This is very rare case, when some default value is given, meaning that this
548+
value will be populated to the field, but if completely deleted, than `Null` will be
549+
saved in database.
550+
551+
```python
552+
"""dict_demo.py"""
553+
from example_app.models import db
554+
555+
556+
def get_default_dict():
557+
"""Example of default dict specification."""
558+
return {"alpha": 1, "text": "text", "float": 1.2}
559+
560+
561+
class DictDemoModel(db.Document):
562+
"""Documentation example model."""
563+
564+
null_dict_default = db.DictField(default=get_default_dict, null=True)
565+
```
411566

412567
## EmailField
413568

example_app/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from example_app import views
66
from example_app.boolean_demo import boolean_demo_view
77
from example_app.dates_demo import dates_demo_view
8+
from example_app.dict_demo import dict_demo_view
89
from example_app.models import db
910
from example_app.numbers_demo import numbers_demo_view
1011
from example_app.strings_demo import strings_demo_view
@@ -52,6 +53,8 @@
5253
app.add_url_rule("/dates/<pk>/", view_func=dates_demo_view, methods=["GET", "POST"])
5354
app.add_url_rule("/bool", view_func=boolean_demo_view, methods=["GET", "POST"])
5455
app.add_url_rule("/bool/<pk>/", view_func=boolean_demo_view, methods=["GET", "POST"])
56+
app.add_url_rule("/dict", view_func=dict_demo_view, methods=["GET", "POST"])
57+
app.add_url_rule("/dict/<pk>/", view_func=dict_demo_view, methods=["GET", "POST"])
5558

5659
if __name__ == "__main__":
5760
app.run(host="0.0.0.0", port=8000)

example_app/boolean_demo.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Booleans fields demo model."""
22

3-
from flask import render_template, request
4-
53
from example_app.models import db
64

75

@@ -24,25 +22,8 @@ class BooleanDemoModel(db.Document):
2422

2523
def boolean_demo_view(pk=None):
2624
"""Return all fields demonstration."""
27-
form = BooleanDemoForm()
28-
obj = None
29-
if pk:
30-
obj = BooleanDemoModel.objects.get(pk=pk)
31-
form = BooleanDemoForm(obj=obj)
32-
33-
if request.method == "POST" and form.validate_on_submit():
34-
if pk:
35-
form.populate_obj(obj)
36-
obj.save()
37-
else:
38-
form.save()
39-
page_num = int(request.args.get("page") or 1)
40-
page = BooleanDemoModel.objects.paginate(page=page_num, per_page=100)
41-
42-
return render_template(
43-
"form_demo.html",
44-
view=boolean_demo_view.__name__,
45-
page=page,
46-
form=form,
47-
model=BooleanDemoModel,
25+
from example_app.views import demo_view
26+
27+
return demo_view(
28+
model=BooleanDemoModel, view_name=boolean_demo_view.__name__, pk=pk
4829
)

example_app/dates_demo.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Date and DateTime fields demo model."""
22

3-
from flask import render_template, request
43
from wtforms.fields import DateTimeField
54

65
from example_app.models import db
@@ -28,25 +27,6 @@ class DateTimeModel(db.Document):
2827

2928
def dates_demo_view(pk=None):
3029
"""Return all fields demonstration."""
31-
form = DateTimeDemoForm()
32-
obj = None
33-
if pk:
34-
obj = DateTimeModel.objects.get(pk=pk)
35-
form = DateTimeDemoForm(obj=obj)
36-
37-
if request.method == "POST" and form.validate_on_submit():
38-
if pk:
39-
form.populate_obj(obj)
40-
obj.save()
41-
else:
42-
form.save()
43-
page_num = int(request.args.get("page") or 1)
44-
page = DateTimeModel.objects.paginate(page=page_num, per_page=100)
45-
46-
return render_template(
47-
"form_demo.html",
48-
view=dates_demo_view.__name__,
49-
page=page,
50-
form=form,
51-
model=DateTimeModel,
52-
)
30+
from example_app.views import demo_view
31+
32+
return demo_view(model=DateTimeModel, view_name=dates_demo_view.__name__, pk=pk)

example_app/dict_demo.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Dict fields demo model."""
2+
3+
from example_app.models import db
4+
5+
6+
def get_default_dict():
7+
"""Example of default dict specification."""
8+
return {"alpha": 1, "text": "text", "float": 1.2}
9+
10+
11+
class DictDemoModel(db.Document):
12+
"""Documentation example model."""
13+
14+
string = db.StringField(verbose_name="str")
15+
dict_field = db.DictField()
16+
no_dict_field = db.DictField(default=None)
17+
null_dict_field = db.DictField(default=None, null=True)
18+
dict_default = db.DictField(default=get_default_dict)
19+
null_dict_default = db.DictField(default=get_default_dict, null=True)
20+
no_dict_prefilled = db.DictField(
21+
default=None,
22+
null=False,
23+
wtf_options={"default": get_default_dict, "null": True},
24+
)
25+
26+
27+
DictDemoForm = DictDemoModel.to_wtf_form()
28+
29+
30+
def dict_demo_view(pk=None):
31+
"""Return all fields demonstration."""
32+
from example_app.views import demo_view
33+
34+
return demo_view(model=DictDemoModel, view_name=dict_demo_view.__name__, pk=pk)

example_app/numbers_demo.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from decimal import Decimal
44

5-
from flask import render_template, request
6-
75
from example_app.models import db
86

97

@@ -26,25 +24,8 @@ class NumbersDemoModel(db.Document):
2624

2725
def numbers_demo_view(pk=None):
2826
"""Return all fields demonstration."""
29-
form = NumbersDemoForm()
30-
obj = None
31-
if pk:
32-
obj = NumbersDemoModel.objects.get(pk=pk)
33-
form = NumbersDemoForm(obj=obj)
34-
35-
if request.method == "POST" and form.validate_on_submit():
36-
if pk:
37-
form.populate_obj(obj)
38-
obj.save()
39-
else:
40-
form.save()
41-
page_num = int(request.args.get("page") or 1)
42-
page = NumbersDemoModel.objects.paginate(page=page_num, per_page=100)
43-
44-
return render_template(
45-
"form_demo.html",
46-
view=numbers_demo_view.__name__,
47-
page=page,
48-
form=form,
49-
model=NumbersDemoModel,
27+
from example_app.views import demo_view
28+
29+
return demo_view(
30+
model=NumbersDemoModel, view_name=numbers_demo_view.__name__, pk=pk
5031
)

example_app/strings_demo.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Strings and strings related fields demo model."""
22
import re
33

4-
from flask import render_template, request
5-
64
from example_app.models import db
75
from flask_mongoengine.wtf import fields as mongo_fields
86

@@ -32,25 +30,8 @@ class StringsDemoModel(db.Document):
3230

3331
def strings_demo_view(pk=None):
3432
"""Return all fields demonstration."""
35-
form = StringsDemoForm()
36-
obj = None
37-
if pk:
38-
obj = StringsDemoModel.objects.get(pk=pk)
39-
form = StringsDemoForm(obj=obj)
40-
41-
if request.method == "POST" and form.validate_on_submit():
42-
if pk:
43-
form.populate_obj(obj)
44-
obj.save()
45-
else:
46-
form.save()
47-
page_num = int(request.args.get("page") or 1)
48-
page = StringsDemoModel.objects.paginate(page=page_num, per_page=100)
49-
50-
return render_template(
51-
"form_demo.html",
52-
view=strings_demo_view.__name__,
53-
page=page,
54-
form=form,
55-
model=StringsDemoModel,
33+
from example_app.views import demo_view
34+
35+
return demo_view(
36+
model=StringsDemoModel, view_name=strings_demo_view.__name__, pk=pk
5637
)

example_app/templates/layout.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<li><a href="{{ url_for("numbers_demo_view") }}">Numbers demo</a></li>
2323
<li><a href="{{ url_for("dates_demo_view") }}">DateTime demo</a></li>
2424
<li><a href="{{ url_for("boolean_demo_view") }}">Booleans demo</a></li>
25+
<li><a href="{{ url_for("dict_demo_view") }}">Dict/Json demo</a></li>
2526
</ul>
2627
</nav>
2728
<div>

0 commit comments

Comments
 (0)