edit.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. from __future__ import absolute_import
  2. import copy
  3. from crispy_forms.utils import TEMPLATE_PACK
  4. from django import forms
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.core.exceptions import PermissionDenied, FieldError
  7. from django.db import models, transaction
  8. from django.forms.models import modelform_factory, modelform_defines_fields
  9. from django.http import Http404, HttpResponseRedirect
  10. from django.template.response import TemplateResponse
  11. from django.utils import six
  12. from django.utils.encoding import force_text
  13. from django.utils.html import escape
  14. from django.utils.text import capfirst, get_text_list
  15. from django.template import loader
  16. from django.utils.translation import ugettext as _
  17. from django.forms.widgets import Media
  18. from xadmin import widgets
  19. from xadmin.layout import FormHelper, Layout, Fieldset, TabHolder, Container, Column, Col, Field
  20. from xadmin.util import unquote
  21. from xadmin.views.detail import DetailAdminUtil
  22. from .base import ModelAdminView, filter_hook, csrf_protect_m
  23. FORMFIELD_FOR_DBFIELD_DEFAULTS = {
  24. models.DateTimeField: {
  25. 'form_class': forms.SplitDateTimeField,
  26. 'widget': widgets.AdminSplitDateTime
  27. },
  28. models.DateField: {'widget': widgets.AdminDateWidget},
  29. models.TimeField: {'widget': widgets.AdminTimeWidget},
  30. models.TextField: {'widget': widgets.AdminTextareaWidget},
  31. models.URLField: {'widget': widgets.AdminURLFieldWidget},
  32. models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
  33. models.BigIntegerField: {'widget': widgets.AdminIntegerFieldWidget},
  34. models.CharField: {'widget': widgets.AdminTextInputWidget},
  35. models.IPAddressField: {'widget': widgets.AdminTextInputWidget},
  36. models.ImageField: {'widget': widgets.AdminFileWidget},
  37. models.FileField: {'widget': widgets.AdminFileWidget},
  38. models.ForeignKey: {'widget': widgets.AdminSelectWidget},
  39. models.OneToOneField: {'widget': widgets.AdminSelectWidget},
  40. models.ManyToManyField: {'widget': widgets.AdminSelectMultiple},
  41. }
  42. class ReadOnlyField(Field):
  43. template = "xadmin/layout/field_value.html"
  44. def __init__(self, *args, **kwargs):
  45. self.detail = kwargs.pop('detail')
  46. super(ReadOnlyField, self).__init__(*args, **kwargs)
  47. def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
  48. html = ''
  49. for field in self.fields:
  50. result = self.detail.get_field_result(field)
  51. field = {'auto_id': field}
  52. html += loader.render_to_string(
  53. self.template, {'field': field, 'result': result})
  54. return html
  55. class ModelFormAdminView(ModelAdminView):
  56. form = forms.ModelForm
  57. formfield_overrides = {}
  58. readonly_fields = ()
  59. style_fields = {}
  60. exclude = None
  61. relfield_style = None
  62. save_as = False
  63. save_on_top = False
  64. add_form_template = None
  65. change_form_template = None
  66. form_layout = None
  67. def __init__(self, request, *args, **kwargs):
  68. overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
  69. overrides.update(self.formfield_overrides)
  70. self.formfield_overrides = overrides
  71. super(ModelFormAdminView, self).__init__(request, *args, **kwargs)
  72. @filter_hook
  73. def formfield_for_dbfield(self, db_field, **kwargs):
  74. # If it uses an intermediary model that isn't auto created, don't show
  75. # a field in admin.
  76. if isinstance(db_field, models.ManyToManyField) and not db_field.remote_field.through._meta.auto_created:
  77. return None
  78. attrs = self.get_field_attrs(db_field, **kwargs)
  79. return db_field.formfield(**dict(attrs, **kwargs))
  80. @filter_hook
  81. def get_field_style(self, db_field, style, **kwargs):
  82. if style in ('radio', 'radio-inline') and (db_field.choices or isinstance(db_field, models.ForeignKey)):
  83. attrs = {'widget': widgets.AdminRadioSelect(
  84. attrs={'inline': 'inline' if style == 'radio-inline' else ''})}
  85. if db_field.choices:
  86. attrs['choices'] = db_field.get_choices(
  87. include_blank=db_field.blank,
  88. blank_choice=[('', _('Null'))]
  89. )
  90. return attrs
  91. if style in ('checkbox', 'checkbox-inline') and isinstance(db_field, models.ManyToManyField):
  92. return {'widget': widgets.AdminCheckboxSelect(attrs={'inline': style == 'checkbox-inline'}),
  93. 'help_text': None}
  94. @filter_hook
  95. def get_field_attrs(self, db_field, **kwargs):
  96. if db_field.name in self.style_fields:
  97. attrs = self.get_field_style(
  98. db_field, self.style_fields[db_field.name], **kwargs)
  99. if attrs:
  100. return attrs
  101. if hasattr(db_field, "rel") and db_field.rel:
  102. related_modeladmin = self.admin_site._registry.get(db_field.rel.to)
  103. if related_modeladmin and hasattr(related_modeladmin, 'relfield_style'):
  104. attrs = self.get_field_style(
  105. db_field, related_modeladmin.relfield_style, **kwargs)
  106. if attrs:
  107. return attrs
  108. if db_field.choices:
  109. return {'widget': widgets.AdminSelectWidget}
  110. for klass in db_field.__class__.mro():
  111. if klass in self.formfield_overrides:
  112. return self.formfield_overrides[klass].copy()
  113. return {}
  114. @filter_hook
  115. def prepare_form(self):
  116. self.model_form = self.get_model_form()
  117. @filter_hook
  118. def instance_forms(self):
  119. self.form_obj = self.model_form(**self.get_form_datas())
  120. def setup_forms(self):
  121. helper = self.get_form_helper()
  122. if helper:
  123. self.form_obj.helper = helper
  124. @filter_hook
  125. def valid_forms(self):
  126. return self.form_obj.is_valid()
  127. @filter_hook
  128. def get_model_form(self, **kwargs):
  129. """
  130. Returns a Form class for use in the admin add view. This is used by
  131. add_view and change_view.
  132. """
  133. if self.exclude is None:
  134. exclude = []
  135. else:
  136. exclude = list(self.exclude)
  137. exclude.extend(self.get_readonly_fields())
  138. if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
  139. # Take the custom ModelForm's Meta.exclude into account only if the
  140. # ModelAdmin doesn't define its own.
  141. exclude.extend(self.form._meta.exclude)
  142. # if exclude is an empty list we pass None to be consistant with the
  143. # default on modelform_factory
  144. exclude = exclude or None
  145. defaults = {
  146. "form": self.form,
  147. "fields": self.fields and list(self.fields) or None,
  148. "exclude": exclude,
  149. "formfield_callback": self.formfield_for_dbfield,
  150. }
  151. defaults.update(kwargs)
  152. if defaults['fields'] is None and not modelform_defines_fields(defaults['form']):
  153. defaults['fields'] = forms.ALL_FIELDS
  154. return modelform_factory(self.model, **defaults)
  155. try:
  156. return modelform_factory(self.model, **defaults)
  157. except FieldError as e:
  158. raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.'
  159. % (e, self.__class__.__name__))
  160. @filter_hook
  161. def get_form_layout(self):
  162. layout = copy.deepcopy(self.form_layout)
  163. arr = self.form_obj.fields.keys()
  164. if six.PY3:
  165. arr = [k for k in arr]
  166. fields = arr + list(self.get_readonly_fields())
  167. if layout is None:
  168. layout = Layout(Container(Col('full',
  169. Fieldset("", *fields, css_class="unsort no_title"), horizontal=True, span=12)
  170. ))
  171. elif type(layout) in (list, tuple) and len(layout) > 0:
  172. if isinstance(layout[0], Column):
  173. fs = layout
  174. elif isinstance(layout[0], (Fieldset, TabHolder)):
  175. fs = (Col('full', *layout, horizontal=True, span=12),)
  176. else:
  177. fs = (Col('full', Fieldset("", *layout, css_class="unsort no_title"), horizontal=True, span=12),)
  178. layout = Layout(Container(*fs))
  179. rendered_fields = [i[1] for i in layout.get_field_names()]
  180. container = layout[0].fields
  181. other_fieldset = Fieldset(_(u'Other Fields'), *[f for f in fields if f not in rendered_fields])
  182. if len(other_fieldset.fields):
  183. if len(container) and isinstance(container[0], Column):
  184. container[0].fields.append(other_fieldset)
  185. else:
  186. container.append(other_fieldset)
  187. return layout
  188. def get_form_helper(self):
  189. helper = FormHelper()
  190. helper.form_tag = False
  191. helper.include_media = False
  192. helper.add_layout(self.get_form_layout())
  193. # deal with readonly fields
  194. readonly_fields = self.get_readonly_fields()
  195. if readonly_fields:
  196. detail = self.get_model_view(
  197. DetailAdminUtil, self.model, self.form_obj.instance)
  198. for field in readonly_fields:
  199. helper[field].wrap(ReadOnlyField, detail=detail)
  200. return helper
  201. @filter_hook
  202. def get_readonly_fields(self):
  203. """
  204. Hook for specifying custom readonly fields.
  205. """
  206. return self.readonly_fields
  207. @filter_hook
  208. def save_forms(self):
  209. self.new_obj = self.form_obj.save(commit=False)
  210. @filter_hook
  211. def change_message(self):
  212. change_message = []
  213. if self.org_obj is None:
  214. change_message.append(_('Added.'))
  215. elif self.form_obj.changed_data:
  216. change_message.append(_('Changed %s.') % get_text_list(self.form_obj.changed_data, _('and')))
  217. change_message = ' '.join(change_message)
  218. return change_message or _('No fields changed.')
  219. @filter_hook
  220. def save_models(self):
  221. self.new_obj.save()
  222. flag = self.org_obj is None and 'create' or 'change'
  223. self.log(flag, self.change_message(), self.new_obj)
  224. @filter_hook
  225. def save_related(self):
  226. self.form_obj.save_m2m()
  227. @csrf_protect_m
  228. @filter_hook
  229. def get(self, request, *args, **kwargs):
  230. self.instance_forms()
  231. self.setup_forms()
  232. return self.get_response()
  233. @csrf_protect_m
  234. @transaction.atomic
  235. @filter_hook
  236. def post(self, request, *args, **kwargs):
  237. self.instance_forms()
  238. self.setup_forms()
  239. if self.valid_forms():
  240. self.save_forms()
  241. self.save_models()
  242. self.save_related()
  243. response = self.post_response()
  244. cls_str = str if six.PY3 else basestring
  245. if isinstance(response, cls_str):
  246. return HttpResponseRedirect(response)
  247. else:
  248. return response
  249. return self.get_response()
  250. @filter_hook
  251. def get_context(self):
  252. add = self.org_obj is None
  253. change = self.org_obj is not None
  254. new_context = {
  255. 'form': self.form_obj,
  256. 'original': self.org_obj,
  257. 'show_delete': self.org_obj is not None,
  258. 'add': add,
  259. 'change': change,
  260. 'errors': self.get_error_list(),
  261. 'has_add_permission': self.has_add_permission(),
  262. 'has_view_permission': self.has_view_permission(),
  263. 'has_change_permission': self.has_change_permission(self.org_obj),
  264. 'has_delete_permission': self.has_delete_permission(self.org_obj),
  265. 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
  266. 'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
  267. 'form_url': '',
  268. 'content_type_id': ContentType.objects.get_for_model(self.model).id,
  269. 'save_as': self.save_as,
  270. 'save_on_top': self.save_on_top,
  271. }
  272. # for submit line
  273. new_context.update({
  274. 'onclick_attrib': '',
  275. 'show_delete_link': (new_context['has_delete_permission']
  276. and (change or new_context['show_delete'])),
  277. 'show_save_as_new': change and self.save_as,
  278. 'show_save_and_add_another': new_context['has_add_permission'] and
  279. (not self.save_as or add),
  280. 'show_save_and_continue': new_context['has_change_permission'],
  281. 'show_save': True
  282. })
  283. if self.org_obj and new_context['show_delete_link']:
  284. new_context['delete_url'] = self.model_admin_url(
  285. 'delete', self.org_obj.pk)
  286. context = super(ModelFormAdminView, self).get_context()
  287. context.update(new_context)
  288. return context
  289. @filter_hook
  290. def get_error_list(self):
  291. errors = forms.utils.ErrorList()
  292. if self.form_obj.is_bound:
  293. errors.extend(self.form_obj.errors.values())
  294. return errors
  295. @filter_hook
  296. def get_media(self):
  297. try:
  298. m = self.form_obj.media
  299. except:
  300. m = Media()
  301. return super(ModelFormAdminView, self).get_media() + m + \
  302. self.vendor('xadmin.page.form.js', 'xadmin.form.css')
  303. class CreateAdminView(ModelFormAdminView):
  304. def init_request(self, *args, **kwargs):
  305. self.org_obj = None
  306. if not self.has_add_permission():
  307. raise PermissionDenied
  308. # comm method for both get and post
  309. self.prepare_form()
  310. @filter_hook
  311. def get_form_datas(self):
  312. # Prepare the dict of initial data from the request.
  313. # We have to special-case M2Ms as a list of comma-separated PKs.
  314. if self.request_method == 'get':
  315. initial = dict(self.request.GET.items())
  316. for k in initial:
  317. try:
  318. f = self.opts.get_field(k)
  319. except models.FieldDoesNotExist:
  320. continue
  321. if isinstance(f, models.ManyToManyField):
  322. initial[k] = initial[k].split(",")
  323. return {'initial': initial}
  324. else:
  325. return {'data': self.request.POST, 'files': self.request.FILES}
  326. @filter_hook
  327. def get_context(self):
  328. new_context = {
  329. 'title': _('Add %s') % force_text(self.opts.verbose_name),
  330. }
  331. context = super(CreateAdminView, self).get_context()
  332. context.update(new_context)
  333. return context
  334. @filter_hook
  335. def get_breadcrumb(self):
  336. bcs = super(ModelFormAdminView, self).get_breadcrumb()
  337. item = {'title': _('Add %s') % force_text(self.opts.verbose_name)}
  338. if self.has_add_permission():
  339. item['url'] = self.model_admin_url('add')
  340. bcs.append(item)
  341. return bcs
  342. @filter_hook
  343. def get_response(self):
  344. context = self.get_context()
  345. context.update(self.kwargs or {})
  346. return TemplateResponse(
  347. self.request, self.add_form_template or self.get_template_list(
  348. 'views/model_form.html'),
  349. context)
  350. @filter_hook
  351. def post_response(self):
  352. """
  353. Determines the HttpResponse for the add_view stage.
  354. """
  355. request = self.request
  356. msg = _(
  357. 'The %(name)s "%(obj)s" was added successfully.') % {'name': force_text(self.opts.verbose_name),
  358. 'obj': "<a class='alert-link' href='%s'>%s</a>" % (self.model_admin_url('change', self.new_obj._get_pk_val()), force_text(self.new_obj))}
  359. if "_continue" in request.POST:
  360. self.message_user(
  361. msg + ' ' + _("You may edit it again below."), 'success')
  362. return self.model_admin_url('change', self.new_obj._get_pk_val())
  363. if "_addanother" in request.POST:
  364. self.message_user(msg + ' ' + (_("You may add another %s below.") % force_text(self.opts.verbose_name)), 'success')
  365. return request.path
  366. else:
  367. self.message_user(msg, 'success')
  368. # Figure out where to redirect. If the user has change permission,
  369. # redirect to the change-list page for this object. Otherwise,
  370. # redirect to the admin index.
  371. if "_redirect" in request.POST:
  372. return request.POST["_redirect"]
  373. elif self.has_view_permission():
  374. return self.model_admin_url('changelist')
  375. else:
  376. return self.get_admin_url('index')
  377. class UpdateAdminView(ModelFormAdminView):
  378. def init_request(self, object_id, *args, **kwargs):
  379. self.org_obj = self.get_object(unquote(object_id))
  380. if not self.has_change_permission(self.org_obj):
  381. raise PermissionDenied
  382. if self.org_obj is None:
  383. raise Http404(_('%(name)s object with primary key %(key)r does not exist.') %
  384. {'name': force_text(self.opts.verbose_name), 'key': escape(object_id)})
  385. # comm method for both get and post
  386. self.prepare_form()
  387. @filter_hook
  388. def get_form_datas(self):
  389. params = {'instance': self.org_obj}
  390. if self.request_method == 'post':
  391. params.update(
  392. {'data': self.request.POST, 'files': self.request.FILES})
  393. return params
  394. @filter_hook
  395. def get_context(self):
  396. new_context = {
  397. 'title': _('Change %s') % force_text(self.org_obj),
  398. 'object_id': str(self.org_obj.pk),
  399. }
  400. context = super(UpdateAdminView, self).get_context()
  401. context.update(new_context)
  402. return context
  403. @filter_hook
  404. def get_breadcrumb(self):
  405. bcs = super(ModelFormAdminView, self).get_breadcrumb()
  406. item = {'title': force_text(self.org_obj)}
  407. if self.has_change_permission():
  408. item['url'] = self.model_admin_url('change', self.org_obj.pk)
  409. bcs.append(item)
  410. return bcs
  411. @filter_hook
  412. def get_response(self, *args, **kwargs):
  413. context = self.get_context()
  414. context.update(kwargs or {})
  415. return TemplateResponse(
  416. self.request, self.change_form_template or self.get_template_list(
  417. 'views/model_form.html'),
  418. context)
  419. def post(self, request, *args, **kwargs):
  420. if "_saveasnew" in self.request.POST:
  421. return self.get_model_view(CreateAdminView, self.model).post(request)
  422. return super(UpdateAdminView, self).post(request, *args, **kwargs)
  423. @filter_hook
  424. def post_response(self):
  425. """
  426. Determines the HttpResponse for the change_view stage.
  427. """
  428. opts = self.new_obj._meta
  429. obj = self.new_obj
  430. request = self.request
  431. verbose_name = opts.verbose_name
  432. pk_value = obj._get_pk_val()
  433. msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name':
  434. force_text(verbose_name), 'obj': force_text(obj)}
  435. if "_continue" in request.POST:
  436. self.message_user(
  437. msg + ' ' + _("You may edit it again below."), 'success')
  438. return request.path
  439. elif "_addanother" in request.POST:
  440. self.message_user(msg + ' ' + (_("You may add another %s below.")
  441. % force_text(verbose_name)), 'success')
  442. return self.model_admin_url('add')
  443. else:
  444. self.message_user(msg, 'success')
  445. # Figure out where to redirect. If the user has change permission,
  446. # redirect to the change-list page for this object. Otherwise,
  447. # redirect to the admin index.
  448. if "_redirect" in request.POST:
  449. return request.POST["_redirect"]
  450. elif self.has_view_permission():
  451. change_list_url = self.model_admin_url('changelist')
  452. if 'LIST_QUERY' in self.request.session \
  453. and self.request.session['LIST_QUERY'][0] == self.model_info:
  454. change_list_url += '?' + self.request.session['LIST_QUERY'][1]
  455. return change_list_url
  456. else:
  457. return self.get_admin_url('index')
  458. class ModelFormAdminUtil(ModelFormAdminView):
  459. def init_request(self, obj=None):
  460. self.org_obj = obj
  461. self.prepare_form()
  462. self.instance_forms()
  463. @filter_hook
  464. def get_form_datas(self):
  465. return {'instance': self.org_obj}