inline.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import copy
  2. import inspect
  3. from django import forms
  4. from django.forms.formsets import all_valid, DELETION_FIELD_NAME
  5. from django.forms.models import inlineformset_factory, BaseInlineFormSet, modelform_defines_fields
  6. from django.contrib.contenttypes.forms import BaseGenericInlineFormSet, generic_inlineformset_factory
  7. from django.template import loader
  8. from django.template.loader import render_to_string
  9. from django.contrib.auth import get_permission_codename
  10. from django.utils import six
  11. from django.utils.encoding import smart_text
  12. from crispy_forms.utils import TEMPLATE_PACK
  13. from xadmin.layout import FormHelper, Layout, flatatt, Container, Column, Field, Fieldset
  14. from xadmin.plugins.utils import get_context_dict
  15. from xadmin.sites import site
  16. from xadmin.views import BaseAdminPlugin, ModelFormAdminView, DetailAdminView, filter_hook
  17. class ShowField(Field):
  18. template = "xadmin/layout/field_value.html"
  19. def __init__(self, admin_view, *args, **kwargs):
  20. super(ShowField, self).__init__(*args, **kwargs)
  21. self.admin_view = admin_view
  22. if admin_view.style == 'table':
  23. self.template = "xadmin/layout/field_value_td.html"
  24. def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
  25. html = ''
  26. detail = form.detail
  27. for field in self.fields:
  28. if not isinstance(form.fields[field].widget, forms.HiddenInput):
  29. result = detail.get_field_result(field)
  30. html += loader.render_to_string(
  31. self.template, context={'field': form[field], 'result': result})
  32. return html
  33. class DeleteField(Field):
  34. def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
  35. if form.instance.pk:
  36. self.attrs['type'] = 'hidden'
  37. return super(DeleteField, self).render(form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs)
  38. else:
  39. return ""
  40. class TDField(Field):
  41. template = "xadmin/layout/td-field.html"
  42. class InlineStyleManager(object):
  43. inline_styles = {}
  44. def register_style(self, name, style):
  45. self.inline_styles[name] = style
  46. def get_style(self, name='stacked'):
  47. return self.inline_styles.get(name)
  48. style_manager = InlineStyleManager()
  49. class InlineStyle(object):
  50. template = 'xadmin/edit_inline/stacked.html'
  51. def __init__(self, view, formset):
  52. self.view = view
  53. self.formset = formset
  54. def update_layout(self, helper):
  55. pass
  56. def get_attrs(self):
  57. return {}
  58. style_manager.register_style('stacked', InlineStyle)
  59. class OneInlineStyle(InlineStyle):
  60. template = 'xadmin/edit_inline/one.html'
  61. style_manager.register_style("one", OneInlineStyle)
  62. class AccInlineStyle(InlineStyle):
  63. template = 'xadmin/edit_inline/accordion.html'
  64. style_manager.register_style("accordion", AccInlineStyle)
  65. class TabInlineStyle(InlineStyle):
  66. template = 'xadmin/edit_inline/tab.html'
  67. style_manager.register_style("tab", TabInlineStyle)
  68. class TableInlineStyle(InlineStyle):
  69. template = 'xadmin/edit_inline/tabular.html'
  70. def update_layout(self, helper):
  71. helper.add_layout(
  72. Layout(*[TDField(f) for f in self.formset[0].fields.keys()]))
  73. def get_attrs(self):
  74. fields = []
  75. readonly_fields = []
  76. if len(self.formset):
  77. fields = [f for k, f in self.formset[0].fields.items() if k != DELETION_FIELD_NAME]
  78. readonly_fields = [f for f in getattr(self.formset[0], 'readonly_fields', [])]
  79. return {
  80. 'fields': fields,
  81. 'readonly_fields': readonly_fields
  82. }
  83. style_manager.register_style("table", TableInlineStyle)
  84. def replace_field_to_value(layout, av):
  85. if layout:
  86. cls_str = str if six.PY3 else basestring
  87. for i, lo in enumerate(layout.fields):
  88. if isinstance(lo, Field) or issubclass(lo.__class__, Field):
  89. layout.fields[i] = ShowField(av, *lo.fields, **lo.attrs)
  90. elif isinstance(lo, cls_str):
  91. layout.fields[i] = ShowField(av, lo)
  92. elif hasattr(lo, 'get_field_names'):
  93. replace_field_to_value(lo, av)
  94. class InlineModelAdmin(ModelFormAdminView):
  95. fk_name = None
  96. formset = BaseInlineFormSet
  97. extra = 3
  98. max_num = None
  99. can_delete = True
  100. fields = []
  101. admin_view = None
  102. style = 'stacked'
  103. def init(self, admin_view):
  104. self.admin_view = admin_view
  105. self.parent_model = admin_view.model
  106. self.org_obj = getattr(admin_view, 'org_obj', None)
  107. self.model_instance = self.org_obj or admin_view.model()
  108. return self
  109. @filter_hook
  110. def get_formset(self, **kwargs):
  111. """Returns a BaseInlineFormSet class for use in admin add/change views."""
  112. if self.exclude is None:
  113. exclude = []
  114. else:
  115. exclude = list(self.exclude)
  116. exclude.extend(self.get_readonly_fields())
  117. if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
  118. # Take the custom ModelForm's Meta.exclude into account only if the
  119. # InlineModelAdmin doesn't define its own.
  120. exclude.extend(self.form._meta.exclude)
  121. # if exclude is an empty list we use None, since that's the actual
  122. # default
  123. exclude = exclude or None
  124. can_delete = self.can_delete and self.has_delete_permission()
  125. defaults = {
  126. "form": self.form,
  127. "formset": self.formset,
  128. "fk_name": self.fk_name,
  129. 'fields': forms.ALL_FIELDS,
  130. "exclude": exclude,
  131. "formfield_callback": self.formfield_for_dbfield,
  132. "extra": self.extra,
  133. "max_num": self.max_num,
  134. "can_delete": can_delete,
  135. }
  136. defaults.update(kwargs)
  137. return inlineformset_factory(self.parent_model, self.model, **defaults)
  138. @filter_hook
  139. def instance_form(self, **kwargs):
  140. formset = self.get_formset(**kwargs)
  141. attrs = {
  142. 'instance': self.model_instance,
  143. 'queryset': self.queryset()
  144. }
  145. if self.request_method == 'post':
  146. attrs.update({
  147. 'data': self.request.POST, 'files': self.request.FILES,
  148. 'save_as_new': "_saveasnew" in self.request.POST
  149. })
  150. instance = formset(**attrs)
  151. instance.view = self
  152. helper = FormHelper()
  153. helper.form_tag = False
  154. helper.include_media = False
  155. # override form method to prevent render csrf_token in inline forms, see template 'bootstrap/whole_uni_form.html'
  156. helper.form_method = 'get'
  157. style = style_manager.get_style(
  158. 'one' if self.max_num == 1 else self.style)(self, instance)
  159. style.name = self.style
  160. if len(instance):
  161. layout = copy.deepcopy(self.form_layout)
  162. if layout is None:
  163. layout = Layout(*instance[0].fields.keys())
  164. elif type(layout) in (list, tuple) and len(layout) > 0:
  165. layout = Layout(*layout)
  166. rendered_fields = [i[1] for i in layout.get_field_names()]
  167. layout.extend([f for f in instance[0]
  168. .fields.keys() if f not in rendered_fields])
  169. helper.add_layout(layout)
  170. style.update_layout(helper)
  171. # replace delete field with Dynamic field, for hidden delete field when instance is NEW.
  172. helper[DELETION_FIELD_NAME].wrap(DeleteField)
  173. instance.helper = helper
  174. instance.style = style
  175. readonly_fields = self.get_readonly_fields()
  176. if readonly_fields:
  177. for form in instance:
  178. form.readonly_fields = []
  179. inst = form.save(commit=False)
  180. if inst:
  181. meta_field_names = [field.name for field in inst._meta.get_fields()]
  182. for readonly_field in readonly_fields:
  183. value = None
  184. label = None
  185. if readonly_field in meta_field_names:
  186. label = inst._meta.get_field(readonly_field).verbose_name
  187. value = smart_text(getattr(inst, readonly_field))
  188. elif inspect.ismethod(getattr(inst, readonly_field, None)):
  189. value = getattr(inst, readonly_field)()
  190. label = getattr(getattr(inst, readonly_field), 'short_description', readonly_field)
  191. elif inspect.ismethod(getattr(self, readonly_field, None)):
  192. value = getattr(self, readonly_field)(inst)
  193. label = getattr(getattr(self, readonly_field), 'short_description', readonly_field)
  194. if value:
  195. form.readonly_fields.append({'label': label, 'contents': value})
  196. return instance
  197. def has_auto_field(self, form):
  198. if form._meta.model._meta.has_auto_field:
  199. return True
  200. for parent in form._meta.model._meta.get_parent_list():
  201. if parent._meta.has_auto_field:
  202. return True
  203. return False
  204. def queryset(self):
  205. queryset = super(InlineModelAdmin, self).queryset()
  206. if not self.has_change_permission() and not self.has_view_permission():
  207. queryset = queryset.none()
  208. return queryset
  209. def has_add_permission(self):
  210. if self.opts.auto_created:
  211. return self.has_change_permission()
  212. codename = get_permission_codename('add', self.opts)
  213. return self.user.has_perm("%s.%s" % (self.opts.app_label, codename))
  214. def has_change_permission(self):
  215. opts = self.opts
  216. if opts.auto_created:
  217. for field in opts.fields:
  218. if field.remote_field and field.remote_field.model != self.parent_model:
  219. opts = field.remote_field.model._meta
  220. break
  221. codename = get_permission_codename('change', opts)
  222. return self.user.has_perm("%s.%s" % (opts.app_label, codename))
  223. def has_delete_permission(self):
  224. if self.opts.auto_created:
  225. return self.has_change_permission()
  226. codename = get_permission_codename('delete', self.opts)
  227. return self.user.has_perm("%s.%s" % (self.opts.app_label, codename))
  228. class GenericInlineModelAdmin(InlineModelAdmin):
  229. ct_field = "content_type"
  230. ct_fk_field = "object_id"
  231. formset = BaseGenericInlineFormSet
  232. def get_formset(self, **kwargs):
  233. if self.exclude is None:
  234. exclude = []
  235. else:
  236. exclude = list(self.exclude)
  237. exclude.extend(self.get_readonly_fields())
  238. if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
  239. # Take the custom ModelForm's Meta.exclude into account only if the
  240. # GenericInlineModelAdmin doesn't define its own.
  241. exclude.extend(self.form._meta.exclude)
  242. exclude = exclude or None
  243. can_delete = self.can_delete and self.has_delete_permission()
  244. defaults = {
  245. "ct_field": self.ct_field,
  246. "fk_field": self.ct_fk_field,
  247. "form": self.form,
  248. "formfield_callback": self.formfield_for_dbfield,
  249. "formset": self.formset,
  250. "extra": self.extra,
  251. "can_delete": can_delete,
  252. "can_order": False,
  253. "max_num": self.max_num,
  254. "exclude": exclude,
  255. 'fields': forms.ALL_FIELDS
  256. }
  257. defaults.update(kwargs)
  258. return generic_inlineformset_factory(self.model, **defaults)
  259. class InlineFormset(Fieldset):
  260. def __init__(self, formset, allow_blank=False, **kwargs):
  261. self.fields = []
  262. self.css_class = kwargs.pop('css_class', '')
  263. self.css_id = "%s-group" % formset.prefix
  264. self.template = formset.style.template
  265. self.inline_style = formset.style.name
  266. if allow_blank and len(formset) == 0:
  267. self.template = 'xadmin/edit_inline/blank.html'
  268. self.inline_style = 'blank'
  269. self.formset = formset
  270. self.model = formset.model
  271. self.opts = formset.model._meta
  272. self.flat_attrs = flatatt(kwargs)
  273. self.extra_attrs = formset.style.get_attrs()
  274. def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
  275. context = get_context_dict(context)
  276. context.update(dict(
  277. formset=self,
  278. prefix=self.formset.prefix,
  279. inline_style=self.inline_style,
  280. **self.extra_attrs
  281. ))
  282. return render_to_string(self.template, context)
  283. class Inline(Fieldset):
  284. def __init__(self, rel_model):
  285. self.model = rel_model
  286. self.fields = []
  287. super(Inline, self).__init__(legend="")
  288. def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
  289. return ""
  290. def get_first_field(layout, clz):
  291. for layout_object in layout.fields:
  292. if issubclass(layout_object.__class__, clz):
  293. return layout_object
  294. elif hasattr(layout_object, 'get_field_names'):
  295. gf = get_first_field(layout_object, clz)
  296. if gf:
  297. return gf
  298. def replace_inline_objects(layout, fs):
  299. if not fs:
  300. return
  301. for i, layout_object in enumerate(layout.fields):
  302. if isinstance(layout_object, Inline) and layout_object.model in fs:
  303. layout.fields[i] = fs.pop(layout_object.model)
  304. elif hasattr(layout_object, 'get_field_names'):
  305. replace_inline_objects(layout_object, fs)
  306. class InlineFormsetPlugin(BaseAdminPlugin):
  307. inlines = []
  308. @property
  309. def inline_instances(self):
  310. if not hasattr(self, '_inline_instances'):
  311. inline_instances = []
  312. for inline_class in self.inlines:
  313. inline = self.admin_view.get_view(
  314. (getattr(inline_class, 'generic_inline', False) and GenericInlineModelAdmin or InlineModelAdmin),
  315. inline_class).init(self.admin_view)
  316. if not (inline.has_add_permission() or
  317. inline.has_change_permission() or
  318. inline.has_delete_permission() or
  319. inline.has_view_permission()):
  320. continue
  321. if not inline.has_add_permission():
  322. inline.max_num = 0
  323. inline_instances.append(inline)
  324. self._inline_instances = inline_instances
  325. return self._inline_instances
  326. def instance_forms(self, ret):
  327. self.formsets = []
  328. for inline in self.inline_instances:
  329. if inline.has_change_permission():
  330. self.formsets.append(inline.instance_form())
  331. else:
  332. self.formsets.append(self._get_detail_formset_instance(inline))
  333. self.admin_view.formsets = self.formsets
  334. def valid_forms(self, result):
  335. return all_valid(self.formsets) and result
  336. def save_related(self):
  337. for formset in self.formsets:
  338. formset.instance = self.admin_view.new_obj
  339. formset.save()
  340. def get_context(self, context):
  341. context['inline_formsets'] = self.formsets
  342. return context
  343. def get_error_list(self, errors):
  344. for fs in self.formsets:
  345. errors.extend(fs.non_form_errors())
  346. for errors_in_inline_form in fs.errors:
  347. errors.extend(errors_in_inline_form.values())
  348. return errors
  349. def get_form_layout(self, layout):
  350. allow_blank = isinstance(self.admin_view, DetailAdminView)
  351. # fixed #176 bug, change dict to list
  352. fs = [(f.model, InlineFormset(f, allow_blank)) for f in self.formsets]
  353. replace_inline_objects(layout, fs)
  354. if fs:
  355. container = get_first_field(layout, Column)
  356. if not container:
  357. container = get_first_field(layout, Container)
  358. if not container:
  359. container = layout
  360. # fixed #176 bug, change dict to list
  361. for key, value in fs:
  362. container.append(value)
  363. return layout
  364. def get_media(self, media):
  365. for fs in self.formsets:
  366. media = media + fs.media
  367. if self.formsets:
  368. media = media + self.vendor(
  369. 'xadmin.plugin.formset.js', 'xadmin.plugin.formset.css')
  370. return media
  371. def _get_detail_formset_instance(self, inline):
  372. formset = inline.instance_form(extra=0, max_num=0, can_delete=0)
  373. formset.detail_page = True
  374. if True:
  375. replace_field_to_value(formset.helper.layout, inline)
  376. model = inline.model
  377. opts = model._meta
  378. fake_admin_class = type(str('%s%sFakeAdmin' % (opts.app_label, opts.model_name)), (object, ), {'model': model})
  379. for form in formset.forms:
  380. instance = form.instance
  381. if instance.pk:
  382. form.detail = self.get_view(
  383. DetailAdminUtil, fake_admin_class, instance)
  384. return formset
  385. class DetailAdminUtil(DetailAdminView):
  386. def init_request(self, obj):
  387. self.obj = obj
  388. self.org_obj = obj
  389. class DetailInlineFormsetPlugin(InlineFormsetPlugin):
  390. def get_model_form(self, form, **kwargs):
  391. self.formsets = [self._get_detail_formset_instance(
  392. inline) for inline in self.inline_instances]
  393. return form
  394. site.register_plugin(InlineFormsetPlugin, ModelFormAdminView)
  395. site.register_plugin(DetailInlineFormsetPlugin, DetailAdminView)