123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- import logging
- from flask_wtf import FlaskForm
- from wtforms import (
- BooleanField,
- DateField,
- DateTimeField,
- DecimalField,
- FloatField,
- IntegerField,
- StringField,
- TextAreaField,
- )
- from wtforms import validators
- from .fields import EnumField, QuerySelectField, QuerySelectMultipleField
- from .fieldwidgets import (
- BS3TextAreaFieldWidget,
- BS3TextFieldWidget,
- DatePickerWidget,
- DateTimePickerWidget,
- Select2ManyWidget,
- Select2Widget,
- )
- from .models.mongoengine.fields import MongoFileField, MongoImageField
- from .upload import (
- BS3FileUploadFieldWidget,
- BS3ImageUploadFieldWidget,
- FileUploadField,
- ImageUploadField,
- )
- from .validators import Unique
- try:
- from wtforms.fields.core import _unset_value as unset_value
- except Exception:
- from wtforms.utils import unset_value # noqa: F401
- log = logging.getLogger(__name__)
- class FieldConverter(object):
- """
- Helper class that converts model fields into WTForm fields
- it has a conversion table with type method checks from model
- interfaces, these methods are invoked with a column name
- """
- conversion_table = (
- ("is_image", ImageUploadField, BS3ImageUploadFieldWidget),
- ("is_file", FileUploadField, BS3FileUploadFieldWidget),
- ("is_gridfs_file", MongoFileField, BS3FileUploadFieldWidget),
- ("is_gridfs_image", MongoImageField, BS3ImageUploadFieldWidget),
- ("is_text", TextAreaField, BS3TextAreaFieldWidget),
- ("is_binary", TextAreaField, BS3TextAreaFieldWidget),
- ("is_string", StringField, BS3TextFieldWidget),
- ("is_integer", IntegerField, BS3TextFieldWidget),
- ("is_numeric", DecimalField, BS3TextFieldWidget),
- ("is_float", FloatField, BS3TextFieldWidget),
- ("is_boolean", BooleanField, None),
- ("is_date", DateField, DatePickerWidget),
- ("is_datetime", DateTimeField, DateTimePickerWidget),
- )
- def __init__(
- self, datamodel, colname, label, description, validators, default=None
- ):
- self.datamodel = datamodel
- self.colname = colname
- self.label = label
- self.description = description
- self.validators = validators
- self.default = default
- def convert(self):
- # sqlalchemy.types.Enum inherits from String, therefore `is_enum` must be
- # checked before checking for `is_string`:
- if getattr(self.datamodel, "is_enum")(self.colname):
- col_type = self.datamodel.list_columns[self.colname].type
- return EnumField(
- enum_class=col_type.enum_class,
- enums=col_type.enums,
- label=self.label,
- description=self.description,
- validators=self.validators,
- widget=Select2Widget(),
- default=self.default,
- )
- for type_marker, field, widget in self.conversion_table:
- if getattr(self.datamodel, type_marker)(self.colname):
- if widget:
- return field(
- self.label,
- description=self.description,
- validators=self.validators,
- widget=widget(),
- default=self.default,
- )
- else:
- return field(
- self.label,
- description=self.description,
- validators=self.validators,
- default=self.default,
- )
- log.error("Column %s Type not supported", self.colname)
- class GeneralModelConverter(object):
- """
- Returns a form from a model only one public exposed
- method 'create_form'
- """
- def __init__(self, datamodel):
- self.datamodel = datamodel
- @staticmethod
- def _get_validators(col_name, validators_columns):
- return validators_columns.get(col_name, [])
- @staticmethod
- def _get_description(col_name, description_columns):
- return description_columns.get(col_name, "")
- @staticmethod
- def _get_label(col_name, label_columns):
- return label_columns.get(col_name, "")
- def _get_related_query_func(self, col_name, filter_rel_fields):
- if filter_rel_fields:
- if col_name in filter_rel_fields:
- datamodel = self.datamodel.get_related_interface(col_name)
- filters = datamodel.get_filters().add_filter_list(
- filter_rel_fields[col_name]
- )
- return lambda: datamodel.query(filters)[1]
- return lambda: self.datamodel.get_related_interface(col_name).query()[1]
- def _get_related_pk_func(self, col_name):
- return lambda obj: self.datamodel.get_related_interface(col_name).get_pk_value(
- obj
- )
- def _convert_many_to_one(
- self,
- col_name,
- label,
- description,
- lst_validators,
- filter_rel_fields,
- form_props,
- ):
- """
- Creates a WTForm field for many to one related fields,
- will use a Select box based on a query. Will only
- work with SQLAlchemy interface.
- """
- query_func = self._get_related_query_func(col_name, filter_rel_fields)
- get_pk_func = self._get_related_pk_func(col_name)
- extra_classes = None
- allow_blank = True
- if not self.datamodel.is_nullable(col_name):
- lst_validators.append(validators.DataRequired())
- allow_blank = False
- else:
- lst_validators.append(validators.Optional())
- form_props[col_name] = QuerySelectField(
- label,
- description=description,
- query_func=query_func,
- get_pk_func=get_pk_func,
- allow_blank=allow_blank,
- validators=lst_validators,
- widget=Select2Widget(extra_classes=extra_classes),
- )
- return form_props
- def _convert_many_to_many(
- self,
- col_name,
- label,
- description,
- lst_validators,
- filter_rel_fields,
- form_props,
- ):
- query_func = self._get_related_query_func(col_name, filter_rel_fields)
- get_pk_func = self._get_related_pk_func(col_name)
- form_props[col_name] = QuerySelectMultipleField(
- label,
- description=description,
- query_func=query_func,
- get_pk_func=get_pk_func,
- validators=lst_validators,
- widget=Select2ManyWidget(),
- )
- return form_props
- def _convert_simple(self, col_name, label, description, lst_validators, form_props):
- # Add Validator size
- max = self.datamodel.get_max_length(col_name)
- min = self.datamodel.get_min_length(col_name)
- if max != -1 or min != -1:
- lst_validators.append(validators.Length(max=max, min=min))
- # Add Validator is null
- if not self.datamodel.is_nullable(col_name):
- lst_validators.append(validators.InputRequired())
- else:
- lst_validators.append(validators.Optional())
- # Add Validator is unique
- if self.datamodel.is_unique(col_name):
- lst_validators.append(Unique(self.datamodel, col_name))
- default_value = self.datamodel.get_col_default(col_name)
- fc = FieldConverter(
- self.datamodel,
- col_name,
- label,
- description,
- lst_validators,
- default=default_value,
- )
- form_props[col_name] = fc.convert()
- return form_props
- def _convert_col(
- self,
- col_name,
- label,
- description,
- lst_validators,
- filter_rel_fields,
- form_props,
- ):
- if self.datamodel.is_relation(col_name):
- if self.datamodel.is_relation_many_to_one(
- col_name
- ) or self.datamodel.is_relation_one_to_one(col_name):
- return self._convert_many_to_one(
- col_name,
- label,
- description,
- lst_validators,
- filter_rel_fields,
- form_props,
- )
- elif self.datamodel.is_relation_many_to_many(
- col_name
- ) or self.datamodel.is_relation_one_to_many(col_name):
- return self._convert_many_to_many(
- col_name,
- label,
- description,
- lst_validators,
- filter_rel_fields,
- form_props,
- )
- else:
- log.warning("Relation %s not supported", col_name)
- else:
- return self._convert_simple(
- col_name, label, description, lst_validators, form_props
- )
- def create_form(
- self,
- label_columns=None,
- inc_columns=None,
- description_columns=None,
- validators_columns=None,
- extra_fields=None,
- filter_rel_fields=None,
- ):
- """
- Converts a model to a form given
- :param label_columns:
- A dictionary with the column's labels.
- :param inc_columns:
- A list with the columns to include
- :param description_columns:
- A dictionary with a description for cols.
- :param validators_columns:
- A dictionary with WTForms validators ex::
- validators={'personal_email':EmailValidator}
- :param extra_fields:
- A dictionary containing column names and a WTForm
- Form fields to be added to the form, these fields do not
- exist on the model itself ex::
- extra_fields={'some_col':BooleanField('Some Col', default=False)}
- :param filter_rel_fields:
- A filter to be applied on relationships
- """
- label_columns = label_columns or {}
- inc_columns = inc_columns or []
- description_columns = description_columns or {}
- validators_columns = validators_columns or {}
- extra_fields = extra_fields or {}
- form_props = {}
- for col_name in inc_columns:
- if col_name in extra_fields:
- form_props[col_name] = extra_fields.get(col_name)
- else:
- self._convert_col(
- col_name,
- self._get_label(col_name, label_columns),
- self._get_description(col_name, description_columns),
- self._get_validators(col_name, validators_columns),
- filter_rel_fields,
- form_props,
- )
- return type("DynamicForm", (DynamicForm,), form_props)
- class DynamicForm(FlaskForm):
- """
- Refresh method will force select field to refresh
- """
- @classmethod
- def refresh(self, obj=None):
- form = self(obj=obj)
- return form
|