33"""
44import decimal
55from collections import OrderedDict
6+ from typing import List , Optional , Type
67
78from bson import ObjectId
89from mongoengine import ReferenceField
10+ from mongoengine .base import BaseDocument , DocumentMetaclass
911from wtforms import fields as f
1012from wtforms import validators
1113
@@ -45,8 +47,8 @@ def __init__(self, converters=None):
4547
4648 self .converters = converters
4749
48- def convert (self , model , field , field_args ):
49- kwargs = {
50+ def _generate_convert_base_kwargs (self , field , field_args ) -> dict :
51+ kwargs : dict = {
5052 "label" : getattr (field , "verbose_name" , field .name ),
5153 "description" : getattr (field , "help_text" , None ) or "" ,
5254 "validators" : getattr (field , "validators" , None ) or [],
@@ -56,42 +58,49 @@ def convert(self, model, field, field_args):
5658 if field_args :
5759 kwargs .update (field_args )
5860
59- if kwargs ["validators" ]:
60- # Create a copy of the list since we will be modifying it, and if
61- # validators set as shared list between fields - duplicates/conflicts may
62- # be created.
63- kwargs ["validators" ] = list (kwargs ["validators" ])
64-
61+ # Create a copy of the lists since we will be modifying it, and if
62+ # validators set as shared list between fields - duplicates/conflicts may
63+ # be created.
64+ kwargs ["validators" ] = list (kwargs ["validators" ])
65+ kwargs ["filters" ] = list (kwargs ["filters" ])
6566 if field .required :
6667 kwargs ["validators" ].append (validators .InputRequired ())
6768 else :
6869 kwargs ["validators" ].append (validators .Optional ())
6970
70- ftype = type ( field ). __name__
71+ return kwargs
7172
72- if field .choices :
73- kwargs ["choices" ] = field .choices
74-
75- if ftype in self .converters :
76- kwargs ["coerce" ] = self .coerce (ftype )
77- multiple_field = kwargs .pop ("multiple" , False )
78- radio_field = kwargs .pop ("radio" , False )
79- if multiple_field :
80- return f .SelectMultipleField (** kwargs )
81- if radio_field :
82- return f .RadioField (** kwargs )
83- return f .SelectField (** kwargs )
73+ def _process_convert_for_choice_fields (self , field , field_class , kwargs ):
74+ kwargs ["choices" ] = field .choices
75+ kwargs ["coerce" ] = self .coerce (field_class )
76+ if kwargs .pop ("multiple" , False ):
77+ return f .SelectMultipleField (** kwargs )
78+ if kwargs .pop ("radio" , False ):
79+ return f .RadioField (** kwargs )
80+ return f .SelectField (** kwargs )
8481
82+ def convert (self , model , field , field_args ):
8583 if hasattr (field , "to_form_field" ):
86- return field .to_form_field (model , kwargs )
84+ return field .to_form_field (model , field_args )
85+
86+ field_class = type (field ).__name__
87+
88+ if field_class not in self .converters :
89+ raise NotImplementedError (
90+ f"No converter for: { field_class } , exclude it from form generation."
91+ )
92+
93+ kwargs = self ._generate_convert_base_kwargs (field , field_args )
94+
95+ if field .choices :
96+ return self ._process_convert_for_choice_fields (field , field_class , kwargs )
8797
8898 if hasattr (field , "field" ) and isinstance (field .field , ReferenceField ):
8999 kwargs ["label_modifier" ] = getattr (
90100 model , f"{ field .name } _label_modifier" , None
91101 )
92102
93- if ftype in self .converters :
94- return self .converters [ftype ](model , field , kwargs )
103+ return self .converters [field_class ](model , field , kwargs )
95104
96105 @classmethod
97106 def _string_common (cls , model , field , kwargs ):
@@ -237,46 +246,68 @@ def coerce(self, field_type):
237246 return coercions .get (field_type , str )
238247
239248
240- def model_fields (model , only = None , exclude = None , field_args = None , converter = None ):
249+ def _get_fields_names (
250+ model ,
251+ only : Optional [List [str ]],
252+ exclude : Optional [List [str ]],
253+ ) -> List [str ]:
241254 """
242- Generate a dictionary of fields for a given database model .
255+ Filter fields names for further form generation .
243256
244- See `model_form` docstring for description of parameters.
257+ :param model: Source model class for fields list retrieval
258+ :param only: If provided, only these field names will have fields definition.
259+ :param exclude: If provided, field names will be excluded from fields definition.
260+ All other field names will have fields.
245261 """
246- from mongoengine . base import BaseDocument , DocumentMetaclass
262+ field_names = model . _fields_ordered
247263
248- if not isinstance (model , (BaseDocument , DocumentMetaclass )):
264+ if only :
265+ field_names = [field for field in only if field in field_names ]
266+ elif exclude :
267+ field_names = [field for field in field_names if field not in set (exclude )]
268+
269+ return field_names
270+
271+
272+ def model_fields (
273+ model : Type [BaseDocument ],
274+ only : Optional [List [str ]] = None ,
275+ exclude : Optional [List [str ]] = None ,
276+ field_args = None ,
277+ converter = None ,
278+ ) -> OrderedDict :
279+ """
280+ Generate a dictionary of fields for a given database model.
281+
282+ See :func:`model_form` docstring for description of parameters.
283+ """
284+ if not issubclass (model , (BaseDocument , DocumentMetaclass )):
249285 raise TypeError ("model must be a mongoengine Document schema" )
250286
251287 converter = converter or ModelConverter ()
252288 field_args = field_args or {}
289+ form_fields_dict = OrderedDict ()
290+ # noinspection PyTypeChecker
291+ fields_names = _get_fields_names (model , only , exclude )
253292
254- names = ((k , v .creation_counter ) for k , v in model ._fields .items ())
255- field_names = [n [0 ] for n in sorted (names , key = lambda n : n [1 ])]
293+ for field_name in fields_names :
294+ # noinspection PyUnresolvedReferences
295+ model_field = model ._fields [field_name ]
296+ form_field = converter .convert (model , model_field , field_args .get (field_name ))
297+ if form_field is not None :
298+ form_fields_dict [field_name ] = form_field
256299
257- if only :
258- field_names = [x for x in only if x in set (field_names )]
259- elif exclude :
260- field_names = [x for x in field_names if x not in set (exclude )]
261-
262- field_dict = OrderedDict ()
263- for name in field_names :
264- model_field = model ._fields [name ]
265- field = converter .convert (model , model_field , field_args .get (name ))
266- if field is not None :
267- field_dict [name ] = field
268-
269- return field_dict
300+ return form_fields_dict
270301
271302
272303def model_form (
273- model ,
274- base_class = ModelForm ,
275- only = None ,
276- exclude = None ,
304+ model : Type [ BaseDocument ] ,
305+ base_class : Type [ ModelForm ] = ModelForm ,
306+ only : Optional [ List [ str ]] = None ,
307+ exclude : Optional [ List [ str ]] = None ,
277308 field_args = None ,
278309 converter = None ,
279- ):
310+ ) -> Type [ ModelForm ] :
280311 """
281312 Create a wtforms Form for a given mongoengine Document schema::
282313
@@ -287,7 +318,7 @@ def model_form(
287318 :param model:
288319 A mongoengine Document schema class
289320 :param base_class:
290- Base form class to extend from. Must be a ``wtforms.Form` ` subclass.
321+ Base form class to extend from. Must be a :class:`.ModelForm ` subclass.
291322 :param only:
292323 An optional iterable with the property names that should be included in
293324 the form. Only these properties will have fields.
@@ -299,8 +330,9 @@ def model_form(
299330 to construct each field object.
300331 :param converter:
301332 A converter to generate the fields based on the model properties. If
302- not set, `` ModelConverter` ` is used.
333+ not set, :class:`. ModelConverter` is used.
303334 """
304335 field_dict = model_fields (model , only , exclude , field_args , converter )
305336 field_dict ["model_class" ] = model
337+ # noinspection PyTypeChecker
306338 return type (f"{ model .__name__ } Form" , (base_class ,), field_dict )
0 commit comments