form.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. from wtforms import fields
  2. from peewee import (CharField, DateTimeField, DateField, TimeField,
  3. PrimaryKeyField, ForeignKeyField, BaseModel)
  4. from wtfpeewee.orm import ModelConverter, model_form
  5. from flask_admin import form
  6. from flask_admin._compat import iteritems, itervalues
  7. from flask_admin.model.form import InlineFormAdmin, InlineModelConverterBase
  8. from flask_admin.model.fields import InlineModelFormField, InlineFieldList, AjaxSelectField
  9. from .tools import get_primary_key, get_meta_fields
  10. from .ajax import create_ajax_loader
  11. try:
  12. from playhouse.postgres_ext import JSONField, BinaryJSONField
  13. pg_ext = True
  14. except:
  15. pg_ext = False
  16. class InlineModelFormList(InlineFieldList):
  17. """
  18. Customized inline model form list field.
  19. """
  20. form_field_type = InlineModelFormField
  21. """
  22. Form field type. Override to use custom field for each inline form
  23. """
  24. def __init__(self, form, model, prop, inline_view, **kwargs):
  25. self.form = form
  26. self.model = model
  27. self.prop = prop
  28. self.inline_view = inline_view
  29. self._pk = get_primary_key(model)
  30. super(InlineModelFormList, self).__init__(self.form_field_type(form, self._pk), **kwargs)
  31. def display_row_controls(self, field):
  32. return field.get_pk() is not None
  33. """ bryhoyt removed def process() entirely, because I believe it was buggy
  34. (but worked because another part of the code had a complimentary bug)
  35. and I'm not sure why it was necessary anyway.
  36. If we want it back in, we need to fix the following bogus query:
  37. self.model.select().where(attr == data).execute()
  38. `data` is not an ID, and only happened to be so because we patched it
  39. in in .contribute() below
  40. For reference, .process() introduced in:
  41. https://github.com/flask-admin/flask-admin/commit/2845e4b28cb40b25e2bf544b327f6202dc7e5709
  42. Fixed, brokenly I think, in:
  43. https://github.com/flask-admin/flask-admin/commit/4383eef3ce7eb01878f086928f8773adb9de79f8#diff-f87e7cd76fb9bc48c8681b24f238fb13R30
  44. """
  45. def populate_obj(self, obj, name):
  46. pass
  47. def save_related(self, obj):
  48. model_id = getattr(obj, self._pk)
  49. attr = getattr(self.model, self.prop)
  50. values = self.model.select().where(attr == model_id).execute()
  51. pk_map = dict((str(getattr(v, self._pk)), v) for v in values)
  52. # Handle request data
  53. for field in self.entries:
  54. field_id = field.get_pk()
  55. is_created = field_id not in pk_map
  56. if not is_created:
  57. model = pk_map[field_id]
  58. if self.should_delete(field):
  59. model.delete_instance(recursive=True)
  60. continue
  61. else:
  62. model = self.model()
  63. field.populate_obj(model, None)
  64. # Force relation
  65. setattr(model, self.prop, model_id)
  66. self.inline_view._on_model_change(field, model, is_created)
  67. model.save()
  68. # Recurse, to save multi-level nested inlines
  69. for f in itervalues(field.form._fields):
  70. if f.type == 'InlineModelFormList':
  71. f.save_related(model)
  72. class CustomModelConverter(ModelConverter):
  73. def __init__(self, view, additional=None):
  74. super(CustomModelConverter, self).__init__(additional)
  75. self.view = view
  76. # @todo: This really should be done within wtfpeewee
  77. self.defaults[CharField] = fields.StringField
  78. self.converters[PrimaryKeyField] = self.handle_pk
  79. self.converters[DateTimeField] = self.handle_datetime
  80. self.converters[DateField] = self.handle_date
  81. self.converters[TimeField] = self.handle_time
  82. if pg_ext:
  83. self.converters[JSONField] = self.handle_json
  84. self.converters[BinaryJSONField] = self.handle_json
  85. self.overrides = getattr(self.view, 'form_overrides', None) or {}
  86. def handle_foreign_key(self, model, field, **kwargs):
  87. loader = getattr(self.view, '_form_ajax_refs', {}).get(field.name)
  88. if loader:
  89. if field.null:
  90. kwargs['allow_blank'] = True
  91. return field.name, AjaxSelectField(loader, **kwargs)
  92. return super(CustomModelConverter, self).handle_foreign_key(model, field, **kwargs)
  93. def handle_pk(self, model, field, **kwargs):
  94. kwargs['validators'] = []
  95. return field.name, fields.HiddenField(**kwargs)
  96. def handle_date(self, model, field, **kwargs):
  97. kwargs['widget'] = form.DatePickerWidget()
  98. return field.name, fields.DateField(**kwargs)
  99. def handle_datetime(self, model, field, **kwargs):
  100. kwargs['widget'] = form.DateTimePickerWidget()
  101. return field.name, fields.DateTimeField(**kwargs)
  102. def handle_time(self, model, field, **kwargs):
  103. return field.name, form.TimeField(**kwargs)
  104. def handle_json(self, model, field, **kwargs):
  105. return field.name, form.JSONField(**kwargs)
  106. def get_form(model, converter,
  107. base_class=form.BaseForm,
  108. only=None,
  109. exclude=None,
  110. field_args=None,
  111. allow_pk=False,
  112. extra_fields=None):
  113. """
  114. Create form from peewee model and contribute extra fields, if necessary
  115. """
  116. result = model_form(model,
  117. base_class=base_class,
  118. only=only,
  119. exclude=exclude,
  120. field_args=field_args,
  121. allow_pk=allow_pk,
  122. converter=converter)
  123. if extra_fields:
  124. for name, field in iteritems(extra_fields):
  125. setattr(result, name, form.recreate_field(field))
  126. return result
  127. class InlineModelConverter(InlineModelConverterBase):
  128. """
  129. Inline model form helper.
  130. """
  131. inline_field_list_type = InlineModelFormList
  132. """
  133. Used field list type.
  134. If you want to do some custom rendering of inline field lists,
  135. you can create your own wtforms field and use it instead
  136. """
  137. def get_info(self, p):
  138. info = super(InlineModelConverter, self).get_info(p)
  139. if info is None:
  140. if isinstance(p, BaseModel):
  141. info = InlineFormAdmin(p)
  142. else:
  143. model = getattr(p, 'model', None)
  144. if model is None:
  145. raise Exception('Unknown inline model admin: %s' % repr(p))
  146. attrs = dict()
  147. for attr in dir(p):
  148. if not attr.startswith('_') and attr != 'model':
  149. attrs[attr] = getattr(p, attr)
  150. info = InlineFormAdmin(model, **attrs)
  151. # Resolve AJAX FKs
  152. info._form_ajax_refs = self.process_ajax_refs(info)
  153. return info
  154. def process_ajax_refs(self, info):
  155. refs = getattr(info, 'form_ajax_refs', None)
  156. result = {}
  157. if refs:
  158. for name, opts in iteritems(refs):
  159. new_name = '%s.%s' % (info.model.__name__.lower(), name)
  160. loader = None
  161. if isinstance(opts, (list, tuple)):
  162. loader = create_ajax_loader(info.model, new_name, name, opts)
  163. else:
  164. loader = opts
  165. result[name] = loader
  166. self.view._form_ajax_refs[new_name] = loader
  167. return result
  168. def contribute(self, converter, model, form_class, inline_model):
  169. # Find property from target model to current model
  170. reverse_field = None
  171. info = self.get_info(inline_model)
  172. for field in get_meta_fields(info.model):
  173. field_type = type(field)
  174. if field_type == ForeignKeyField:
  175. if field.rel_model == model:
  176. reverse_field = field
  177. break
  178. else:
  179. raise Exception('Cannot find reverse relation for model %s' % info.model)
  180. # Remove reverse property from the list
  181. ignore = [reverse_field.name]
  182. if info.form_excluded_columns:
  183. exclude = ignore + info.form_excluded_columns
  184. else:
  185. exclude = ignore
  186. # Create field
  187. child_form = info.get_form()
  188. if child_form is None:
  189. child_form = model_form(info.model,
  190. base_class=form.BaseForm,
  191. only=info.form_columns,
  192. exclude=exclude,
  193. field_args=info.form_args,
  194. allow_pk=True,
  195. converter=converter)
  196. prop_name = reverse_field.related_name
  197. label = self.get_label(info, prop_name)
  198. setattr(form_class,
  199. prop_name,
  200. self.inline_field_list_type(child_form,
  201. info.model,
  202. reverse_field.name,
  203. info,
  204. label=label or info.model.__name__))
  205. return form_class
  206. def save_inline(form, model):
  207. for f in itervalues(form._fields):
  208. if f.type == 'InlineModelFormList':
  209. f.save_related(model)