fields.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. """
  2. Useful form fields for use with SQLAlchemy ORM.
  3. """
  4. import operator
  5. from wtforms.fields import SelectFieldBase, StringField
  6. from wtforms.validators import ValidationError
  7. try:
  8. from wtforms.fields import _unset_value as unset_value
  9. except ImportError:
  10. from wtforms.utils import unset_value
  11. from .tools import get_primary_key
  12. from flask_admin._compat import text_type, string_types, iteritems
  13. from flask_admin.form import FormOpts, BaseForm, Select2Widget
  14. from flask_admin.model.fields import InlineFieldList, InlineModelFormField
  15. from flask_admin.babel import lazy_gettext
  16. try:
  17. from sqlalchemy.orm.util import identity_key
  18. has_identity_key = True
  19. except ImportError:
  20. has_identity_key = False
  21. class QuerySelectField(SelectFieldBase):
  22. """
  23. Will display a select drop-down field to choose between ORM results in a
  24. sqlalchemy `Query`. The `data` property actually will store/keep an ORM
  25. model instance, not the ID. Submitting a choice which is not in the query
  26. will result in a validation error.
  27. This field only works for queries on models whose primary key column(s)
  28. have a consistent string representation. This means it mostly only works
  29. for those composed of string, unicode, and integer types. For the most
  30. part, the primary keys will be auto-detected from the model, alternately
  31. pass a one-argument callable to `get_pk` which can return a unique
  32. comparable key.
  33. The `query` property on the field can be set from within a view to assign
  34. a query per-instance to the field. If the property is not set, the
  35. `query_factory` callable passed to the field constructor will be called to
  36. obtain a query.
  37. Specify `get_label` to customize the label associated with each option. If
  38. a string, this is the name of an attribute on the model object to use as
  39. the label text. If a one-argument callable, this callable will be passed
  40. model instance and expected to return the label text. Otherwise, the model
  41. object's `__str__` or `__unicode__` will be used.
  42. If `allow_blank` is set to `True`, then a blank choice will be added to the
  43. top of the list. Selecting this choice will result in the `data` property
  44. being `None`. The label for this blank choice can be set by specifying the
  45. `blank_text` parameter.
  46. """
  47. widget = Select2Widget()
  48. def __init__(self, label=None, validators=None, query_factory=None,
  49. get_pk=None, get_label=None, allow_blank=False,
  50. blank_text=u'', **kwargs):
  51. super(QuerySelectField, self).__init__(label, validators, **kwargs)
  52. self.query_factory = query_factory
  53. if get_pk is None:
  54. if not has_identity_key:
  55. raise Exception(u'The sqlalchemy identity_key function could not be imported.')
  56. self.get_pk = get_pk_from_identity
  57. else:
  58. self.get_pk = get_pk
  59. if get_label is None:
  60. self.get_label = lambda x: x
  61. elif isinstance(get_label, string_types):
  62. self.get_label = operator.attrgetter(get_label)
  63. else:
  64. self.get_label = get_label
  65. self.allow_blank = allow_blank
  66. self.blank_text = blank_text
  67. self.query = None
  68. self._object_list = None
  69. def _get_data(self):
  70. if self._formdata is not None:
  71. for pk, obj in self._get_object_list():
  72. if pk == self._formdata:
  73. self._set_data(obj)
  74. break
  75. return self._data
  76. def _set_data(self, data):
  77. self._data = data
  78. self._formdata = None
  79. data = property(_get_data, _set_data)
  80. def _get_object_list(self):
  81. if self._object_list is None:
  82. query = self.query or self.query_factory()
  83. get_pk = self.get_pk
  84. self._object_list = [(text_type(get_pk(obj)), obj) for obj in query]
  85. return self._object_list
  86. def iter_choices(self):
  87. if self.allow_blank:
  88. yield (u'__None', self.blank_text, self.data is None)
  89. for pk, obj in self._get_object_list():
  90. yield (pk, self.get_label(obj), obj == self.data)
  91. def process_formdata(self, valuelist):
  92. if valuelist:
  93. if self.allow_blank and valuelist[0] == u'__None':
  94. self.data = None
  95. else:
  96. self._data = None
  97. self._formdata = valuelist[0]
  98. def pre_validate(self, form):
  99. if not self.allow_blank or self.data is not None:
  100. for pk, obj in self._get_object_list():
  101. if self.data == obj:
  102. break
  103. else:
  104. raise ValidationError(self.gettext(u'Not a valid choice'))
  105. class QuerySelectMultipleField(QuerySelectField):
  106. """
  107. Very similar to QuerySelectField with the difference that this will
  108. display a multiple select. The data property will hold a list with ORM
  109. model instances and will be an empty list when no value is selected.
  110. If any of the items in the data list or submitted form data cannot be
  111. found in the query, this will result in a validation error.
  112. """
  113. widget = Select2Widget(multiple=True)
  114. def __init__(self, label=None, validators=None, default=None, **kwargs):
  115. if default is None:
  116. default = []
  117. super(QuerySelectMultipleField, self).__init__(label, validators, default=default, **kwargs)
  118. self._invalid_formdata = False
  119. def _get_data(self):
  120. formdata = self._formdata
  121. if formdata is not None:
  122. data = []
  123. for pk, obj in self._get_object_list():
  124. if not formdata:
  125. break
  126. elif pk in formdata:
  127. formdata.remove(pk)
  128. data.append(obj)
  129. if formdata:
  130. self._invalid_formdata = True
  131. self._set_data(data)
  132. return self._data
  133. def _set_data(self, data):
  134. self._data = data
  135. self._formdata = None
  136. data = property(_get_data, _set_data)
  137. def iter_choices(self):
  138. for pk, obj in self._get_object_list():
  139. yield (pk, self.get_label(obj), obj in self.data)
  140. def process_formdata(self, valuelist):
  141. self._formdata = set(valuelist)
  142. def pre_validate(self, form):
  143. if self._invalid_formdata:
  144. raise ValidationError(self.gettext(u'Not a valid choice'))
  145. elif self.data:
  146. obj_list = list(x[1] for x in self._get_object_list())
  147. for v in self.data:
  148. if v not in obj_list:
  149. raise ValidationError(self.gettext(u'Not a valid choice'))
  150. class HstoreForm(BaseForm):
  151. """ Form used in InlineFormField/InlineHstoreList for HSTORE columns """
  152. key = StringField(lazy_gettext('Key'))
  153. value = StringField(lazy_gettext('Value'))
  154. class KeyValue(object):
  155. """ Used by InlineHstoreList to simulate a key and a value field instead of
  156. the single HSTORE column. """
  157. def __init__(self, key=None, value=None):
  158. self.key = key
  159. self.value = value
  160. class InlineHstoreList(InlineFieldList):
  161. """ Version of InlineFieldList for use with Postgres HSTORE columns """
  162. def process(self, formdata, data=unset_value):
  163. """ SQLAlchemy returns a dict for HSTORE columns, but WTForms cannot
  164. process a dict. This overrides `process` to convert the dict
  165. returned by SQLAlchemy to a list of classes before processing. """
  166. if isinstance(data, dict):
  167. data = [KeyValue(k, v) for k, v in iteritems(data)]
  168. super(InlineHstoreList, self).process(formdata, data)
  169. def populate_obj(self, obj, name):
  170. """ Combines each FormField key/value into a dictionary for storage """
  171. _fake = type(str('_fake'), (object, ), {})
  172. output = {}
  173. for form_field in self.entries:
  174. if not self.should_delete(form_field):
  175. fake_obj = _fake()
  176. fake_obj.data = KeyValue()
  177. form_field.populate_obj(fake_obj, 'data')
  178. output[fake_obj.data.key] = fake_obj.data.value
  179. setattr(obj, name, output)
  180. class InlineModelFormList(InlineFieldList):
  181. """
  182. Customized inline model form list field.
  183. """
  184. form_field_type = InlineModelFormField
  185. """
  186. Form field type. Override to use custom field for each inline form
  187. """
  188. def __init__(self, form, session, model, prop, inline_view, **kwargs):
  189. """
  190. Default constructor.
  191. :param form:
  192. Form for the related model
  193. :param session:
  194. SQLAlchemy session
  195. :param model:
  196. Related model
  197. :param prop:
  198. Related property name
  199. :param inline_view:
  200. Inline view
  201. """
  202. self.form = form
  203. self.session = session
  204. self.model = model
  205. self.prop = prop
  206. self.inline_view = inline_view
  207. self._pk = get_primary_key(model)
  208. # Generate inline form field
  209. form_opts = FormOpts(widget_args=getattr(inline_view, 'form_widget_args', None),
  210. form_rules=inline_view._form_rules)
  211. form_field = self.form_field_type(form, self._pk, form_opts=form_opts)
  212. super(InlineModelFormList, self).__init__(form_field, **kwargs)
  213. def display_row_controls(self, field):
  214. return field.get_pk() is not None
  215. def populate_obj(self, obj, name):
  216. values = getattr(obj, name, None)
  217. if values is None:
  218. return
  219. # Create primary key map
  220. pk_map = dict((str(getattr(v, self._pk)), v) for v in values)
  221. # Handle request data
  222. for field in self.entries:
  223. field_id = field.get_pk()
  224. is_created = field_id not in pk_map
  225. if not is_created:
  226. model = pk_map[field_id]
  227. if self.should_delete(field):
  228. self.session.delete(model)
  229. continue
  230. else:
  231. model = self.model()
  232. values.append(model)
  233. field.populate_obj(model, None)
  234. self.inline_view._on_model_change(field, model, is_created)
  235. def get_pk_from_identity(obj):
  236. # TODO: Remove me
  237. cls, key = identity_key(instance=obj)
  238. return u':'.join(text_type(x) for x in key)