forms.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import logging
  2. from flask_wtf import FlaskForm
  3. from wtforms import (
  4. BooleanField,
  5. DateField,
  6. DateTimeField,
  7. DecimalField,
  8. FloatField,
  9. IntegerField,
  10. StringField,
  11. TextAreaField,
  12. )
  13. from wtforms import validators
  14. from .fields import EnumField, QuerySelectField, QuerySelectMultipleField
  15. from .fieldwidgets import (
  16. BS3TextAreaFieldWidget,
  17. BS3TextFieldWidget,
  18. DatePickerWidget,
  19. DateTimePickerWidget,
  20. Select2ManyWidget,
  21. Select2Widget,
  22. )
  23. from .models.mongoengine.fields import MongoFileField, MongoImageField
  24. from .upload import (
  25. BS3FileUploadFieldWidget,
  26. BS3ImageUploadFieldWidget,
  27. FileUploadField,
  28. ImageUploadField,
  29. )
  30. from .validators import Unique
  31. try:
  32. from wtforms.fields.core import _unset_value as unset_value
  33. except Exception:
  34. from wtforms.utils import unset_value # noqa: F401
  35. log = logging.getLogger(__name__)
  36. class FieldConverter(object):
  37. """
  38. Helper class that converts model fields into WTForm fields
  39. it has a conversion table with type method checks from model
  40. interfaces, these methods are invoked with a column name
  41. """
  42. conversion_table = (
  43. ("is_image", ImageUploadField, BS3ImageUploadFieldWidget),
  44. ("is_file", FileUploadField, BS3FileUploadFieldWidget),
  45. ("is_gridfs_file", MongoFileField, BS3FileUploadFieldWidget),
  46. ("is_gridfs_image", MongoImageField, BS3ImageUploadFieldWidget),
  47. ("is_text", TextAreaField, BS3TextAreaFieldWidget),
  48. ("is_binary", TextAreaField, BS3TextAreaFieldWidget),
  49. ("is_string", StringField, BS3TextFieldWidget),
  50. ("is_integer", IntegerField, BS3TextFieldWidget),
  51. ("is_numeric", DecimalField, BS3TextFieldWidget),
  52. ("is_float", FloatField, BS3TextFieldWidget),
  53. ("is_boolean", BooleanField, None),
  54. ("is_date", DateField, DatePickerWidget),
  55. ("is_datetime", DateTimeField, DateTimePickerWidget),
  56. )
  57. def __init__(
  58. self, datamodel, colname, label, description, validators, default=None
  59. ):
  60. self.datamodel = datamodel
  61. self.colname = colname
  62. self.label = label
  63. self.description = description
  64. self.validators = validators
  65. self.default = default
  66. def convert(self):
  67. # sqlalchemy.types.Enum inherits from String, therefore `is_enum` must be
  68. # checked before checking for `is_string`:
  69. if getattr(self.datamodel, "is_enum")(self.colname):
  70. col_type = self.datamodel.list_columns[self.colname].type
  71. return EnumField(
  72. enum_class=col_type.enum_class,
  73. enums=col_type.enums,
  74. label=self.label,
  75. description=self.description,
  76. validators=self.validators,
  77. widget=Select2Widget(),
  78. default=self.default,
  79. )
  80. for type_marker, field, widget in self.conversion_table:
  81. if getattr(self.datamodel, type_marker)(self.colname):
  82. if widget:
  83. return field(
  84. self.label,
  85. description=self.description,
  86. validators=self.validators,
  87. widget=widget(),
  88. default=self.default,
  89. )
  90. else:
  91. return field(
  92. self.label,
  93. description=self.description,
  94. validators=self.validators,
  95. default=self.default,
  96. )
  97. log.error("Column %s Type not supported", self.colname)
  98. class GeneralModelConverter(object):
  99. """
  100. Returns a form from a model only one public exposed
  101. method 'create_form'
  102. """
  103. def __init__(self, datamodel):
  104. self.datamodel = datamodel
  105. @staticmethod
  106. def _get_validators(col_name, validators_columns):
  107. return validators_columns.get(col_name, [])
  108. @staticmethod
  109. def _get_description(col_name, description_columns):
  110. return description_columns.get(col_name, "")
  111. @staticmethod
  112. def _get_label(col_name, label_columns):
  113. return label_columns.get(col_name, "")
  114. def _get_related_query_func(self, col_name, filter_rel_fields):
  115. if filter_rel_fields:
  116. if col_name in filter_rel_fields:
  117. datamodel = self.datamodel.get_related_interface(col_name)
  118. filters = datamodel.get_filters().add_filter_list(
  119. filter_rel_fields[col_name]
  120. )
  121. return lambda: datamodel.query(filters)[1]
  122. return lambda: self.datamodel.get_related_interface(col_name).query()[1]
  123. def _get_related_pk_func(self, col_name):
  124. return lambda obj: self.datamodel.get_related_interface(col_name).get_pk_value(
  125. obj
  126. )
  127. def _convert_many_to_one(
  128. self,
  129. col_name,
  130. label,
  131. description,
  132. lst_validators,
  133. filter_rel_fields,
  134. form_props,
  135. ):
  136. """
  137. Creates a WTForm field for many to one related fields,
  138. will use a Select box based on a query. Will only
  139. work with SQLAlchemy interface.
  140. """
  141. query_func = self._get_related_query_func(col_name, filter_rel_fields)
  142. get_pk_func = self._get_related_pk_func(col_name)
  143. extra_classes = None
  144. allow_blank = True
  145. if not self.datamodel.is_nullable(col_name):
  146. lst_validators.append(validators.DataRequired())
  147. allow_blank = False
  148. else:
  149. lst_validators.append(validators.Optional())
  150. form_props[col_name] = QuerySelectField(
  151. label,
  152. description=description,
  153. query_func=query_func,
  154. get_pk_func=get_pk_func,
  155. allow_blank=allow_blank,
  156. validators=lst_validators,
  157. widget=Select2Widget(extra_classes=extra_classes),
  158. )
  159. return form_props
  160. def _convert_many_to_many(
  161. self,
  162. col_name,
  163. label,
  164. description,
  165. lst_validators,
  166. filter_rel_fields,
  167. form_props,
  168. ):
  169. query_func = self._get_related_query_func(col_name, filter_rel_fields)
  170. get_pk_func = self._get_related_pk_func(col_name)
  171. form_props[col_name] = QuerySelectMultipleField(
  172. label,
  173. description=description,
  174. query_func=query_func,
  175. get_pk_func=get_pk_func,
  176. validators=lst_validators,
  177. widget=Select2ManyWidget(),
  178. )
  179. return form_props
  180. def _convert_simple(self, col_name, label, description, lst_validators, form_props):
  181. # Add Validator size
  182. max = self.datamodel.get_max_length(col_name)
  183. min = self.datamodel.get_min_length(col_name)
  184. if max != -1 or min != -1:
  185. lst_validators.append(validators.Length(max=max, min=min))
  186. # Add Validator is null
  187. if not self.datamodel.is_nullable(col_name):
  188. lst_validators.append(validators.InputRequired())
  189. else:
  190. lst_validators.append(validators.Optional())
  191. # Add Validator is unique
  192. if self.datamodel.is_unique(col_name):
  193. lst_validators.append(Unique(self.datamodel, col_name))
  194. default_value = self.datamodel.get_col_default(col_name)
  195. fc = FieldConverter(
  196. self.datamodel,
  197. col_name,
  198. label,
  199. description,
  200. lst_validators,
  201. default=default_value,
  202. )
  203. form_props[col_name] = fc.convert()
  204. return form_props
  205. def _convert_col(
  206. self,
  207. col_name,
  208. label,
  209. description,
  210. lst_validators,
  211. filter_rel_fields,
  212. form_props,
  213. ):
  214. if self.datamodel.is_relation(col_name):
  215. if self.datamodel.is_relation_many_to_one(
  216. col_name
  217. ) or self.datamodel.is_relation_one_to_one(col_name):
  218. return self._convert_many_to_one(
  219. col_name,
  220. label,
  221. description,
  222. lst_validators,
  223. filter_rel_fields,
  224. form_props,
  225. )
  226. elif self.datamodel.is_relation_many_to_many(
  227. col_name
  228. ) or self.datamodel.is_relation_one_to_many(col_name):
  229. return self._convert_many_to_many(
  230. col_name,
  231. label,
  232. description,
  233. lst_validators,
  234. filter_rel_fields,
  235. form_props,
  236. )
  237. else:
  238. log.warning("Relation %s not supported", col_name)
  239. else:
  240. return self._convert_simple(
  241. col_name, label, description, lst_validators, form_props
  242. )
  243. def create_form(
  244. self,
  245. label_columns=None,
  246. inc_columns=None,
  247. description_columns=None,
  248. validators_columns=None,
  249. extra_fields=None,
  250. filter_rel_fields=None,
  251. ):
  252. """
  253. Converts a model to a form given
  254. :param label_columns:
  255. A dictionary with the column's labels.
  256. :param inc_columns:
  257. A list with the columns to include
  258. :param description_columns:
  259. A dictionary with a description for cols.
  260. :param validators_columns:
  261. A dictionary with WTForms validators ex::
  262. validators={'personal_email':EmailValidator}
  263. :param extra_fields:
  264. A dictionary containing column names and a WTForm
  265. Form fields to be added to the form, these fields do not
  266. exist on the model itself ex::
  267. extra_fields={'some_col':BooleanField('Some Col', default=False)}
  268. :param filter_rel_fields:
  269. A filter to be applied on relationships
  270. """
  271. label_columns = label_columns or {}
  272. inc_columns = inc_columns or []
  273. description_columns = description_columns or {}
  274. validators_columns = validators_columns or {}
  275. extra_fields = extra_fields or {}
  276. form_props = {}
  277. for col_name in inc_columns:
  278. if col_name in extra_fields:
  279. form_props[col_name] = extra_fields.get(col_name)
  280. else:
  281. self._convert_col(
  282. col_name,
  283. self._get_label(col_name, label_columns),
  284. self._get_description(col_name, description_columns),
  285. self._get_validators(col_name, validators_columns),
  286. filter_rel_fields,
  287. form_props,
  288. )
  289. return type("DynamicForm", (DynamicForm,), form_props)
  290. class DynamicForm(FlaskForm):
  291. """
  292. Refresh method will force select field to refresh
  293. """
  294. @classmethod
  295. def refresh(self, obj=None):
  296. form = self(obj=obj)
  297. return form