99
1010from flask import json
1111from mongoengine .queryset import DoesNotExist
12+ from werkzeug .datastructures import FileStorage
1213from wtforms import fields as wtf_fields
1314from wtforms import validators as wtf_validators
1415from 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
1721def 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
3547class 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+
372431class 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-
437487class MongoDictField (MongoTextAreaField ):
438488 """Form field to handle JSON in :class:`~flask_mongoengine.db_fields.DictField`."""
439489
0 commit comments