Skip to content

Commit 1f84fe6

Browse files
committed
Add custom Mongo FileField for Forms, and related widget
1 parent 2d5d523 commit 1f84fe6

File tree

2 files changed

+93
-9
lines changed

2 files changed

+93
-9
lines changed

flask_mongoengine/wtf/fields.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99

1010
from flask import json
1111
from mongoengine.queryset import DoesNotExist
12+
from werkzeug.datastructures import FileStorage
1213
from wtforms import fields as wtf_fields
1314
from wtforms import validators as wtf_validators
1415
from wtforms import widgets as wtf_widgets
16+
from wtforms.utils import unset_value
17+
18+
from flask_mongoengine.wtf import widgets as mongo_widgets
1519

1620

1721
def coerce_boolean(value: Optional[str]) -> Optional[bool]:
@@ -31,6 +35,14 @@ def coerce_boolean(value: Optional[str]) -> Optional[bool]:
3135
raise ValueError("Unexpected string value.")
3236

3337

38+
def _is_empty_file(file_object):
39+
"""Detects empty files and file streams."""
40+
file_object.seek(0)
41+
first_char = file_object.read(1)
42+
file_object.seek(0)
43+
return not bool(first_char)
44+
45+
3446
# noinspection PyAttributeOutsideInit,PyAbstractClass
3547
class QuerySetSelectField(wtf_fields.SelectFieldBase):
3648
"""
@@ -369,6 +381,53 @@ class MongoEmailField(EmptyStringIsNoneMixin, wtf_fields.EmailField):
369381
pass
370382

371383

384+
class MongoFileField(wtf_fields.FileField):
385+
"""GridFS file field."""
386+
387+
widget = mongo_widgets.MongoFileInput()
388+
389+
def __init__(self, **kwargs):
390+
"""Extends base field arguments with file delete marker."""
391+
super().__init__(**kwargs)
392+
393+
self._should_delete = False
394+
self._marker = f"_{self.name}_delete"
395+
396+
def process(self, formdata, data=unset_value, extra_filters=None):
397+
"""Extracts 'delete' marker option, if exists in request."""
398+
if formdata and self._marker in formdata:
399+
self._should_delete = True
400+
return super().process(formdata, data=data, extra_filters=extra_filters)
401+
402+
def populate_obj(self, obj, name):
403+
"""Upload, replace or delete file from database, according form action."""
404+
field = getattr(obj, name, None)
405+
406+
if field is None:
407+
return None
408+
409+
if self._should_delete:
410+
field.delete()
411+
return None
412+
413+
if isinstance(self.data, FileStorage) and not _is_empty_file(self.data.stream):
414+
action = field.replace if field.grid_id else field.put
415+
action(
416+
self.data.stream,
417+
filename=self.data.filename,
418+
content_type=self.data.content_type,
419+
)
420+
421+
422+
class MongoFloatField(wtf_fields.FloatField):
423+
"""
424+
Regular :class:`wtforms.fields.FloatField`, with widget replaced to
425+
:class:`wtforms.widgets.NumberInput`.
426+
"""
427+
428+
widget = wtf_widgets.NumberInput(step="any")
429+
430+
372431
class MongoHiddenField(EmptyStringIsNoneMixin, wtf_fields.HiddenField):
373432
"""
374433
Regular :class:`wtforms.fields.HiddenField`, that transform empty string to `None`.
@@ -425,15 +484,6 @@ class MongoURLField(EmptyStringIsNoneMixin, wtf_fields.URLField):
425484
pass
426485

427486

428-
class MongoFloatField(wtf_fields.FloatField):
429-
"""
430-
Regular :class:`wtforms.fields.FloatField`, with widget replaced to
431-
:class:`wtforms.widgets.NumberInput`.
432-
"""
433-
434-
widget = wtf_widgets.NumberInput(step="any")
435-
436-
437487
class MongoDictField(MongoTextAreaField):
438488
"""Form field to handle JSON in :class:`~flask_mongoengine.db_fields.DictField`."""
439489

flask_mongoengine/wtf/widgets.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Custom widgets for Mongo fields."""
2+
from markupsafe import Markup, escape
3+
from mongoengine.fields import GridFSProxy
4+
from wtforms.widgets import html_params
5+
6+
7+
class MongoFileInput(object):
8+
"""Renders a file input field with delete option."""
9+
10+
template = """
11+
<div>
12+
<i class="icon-file"></i>%(name)s %(size)dk (%(content_type)s)
13+
<input type="checkbox" name="%(marker)s">Delete</input>
14+
</div>
15+
"""
16+
17+
def __call__(self, field, **kwargs):
18+
kwargs.setdefault("id", field.id)
19+
placeholder = ""
20+
if field.data and isinstance(field.data, GridFSProxy):
21+
data = field.data
22+
placeholder = self.template % {
23+
"name": escape(data.name),
24+
"content_type": escape(data.content_type),
25+
"size": data.length // 1024,
26+
"marker": f"_{field.name}_delete",
27+
}
28+
29+
return Markup(
30+
(
31+
"%s<input %s>"
32+
% (placeholder, html_params(name=field.name, type="file", **kwargs))
33+
)
34+
)

0 commit comments

Comments
 (0)