Source code for xadmin.views.edit
# coding=utf-8
import copy
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.forms.models import modelform_factory
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.utils.encoding import force_unicode
from django.utils.html import escape
from django.template import loader
from django.utils.translation import ugettext as _
from xadmin import widgets
from xadmin.layout import FormHelper, Layout, Fieldset, TabHolder, Container, Column, Col, Field
from xadmin.util import unquote
from xadmin.views.detail import DetailAdminUtil
from base import ModelAdminView, filter_hook, csrf_protect_m
#: xadmin在显示 Form 时,系统默认的 DB Field 对应的 Form Field 的属性。
FORMFIELD_FOR_DBFIELD_DEFAULTS = {
models.DateTimeField: {
'form_class': forms.SplitDateTimeField,
'widget': widgets.AdminSplitDateTime
},
models.DateField: {'widget': widgets.AdminDateWidget},
models.TimeField: {'widget': widgets.AdminTimeWidget},
models.TextField: {'widget': widgets.AdminTextareaWidget},
models.URLField: {'widget': widgets.AdminURLFieldWidget},
models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
models.BigIntegerField: {'widget': widgets.AdminIntegerFieldWidget},
models.CharField: {'widget': widgets.AdminTextInputWidget},
models.IPAddressField: {'widget': widgets.AdminTextInputWidget},
models.ImageField: {'widget': widgets.AdminFileWidget},
models.FileField: {'widget': widgets.AdminFileWidget},
models.ForeignKey: {'widget': widgets.AdminSelectWidget},
models.OneToOneField: {'widget': widgets.AdminSelectWidget},
models.ManyToManyField: {'widget': widgets.AdminSelectMultiple},
}
class ReadOnlyField(Field):
"""
crispy Field,使用 :class:`~xadmin.views.detail.DetailAdminView` 仅显示该字段的内容,不能编辑。
"""
template = "xadmin/layout/field_value.html"
def __init__(self, *args, **kwargs):
self.detail = kwargs.pop('detail')
super(ReadOnlyField, self).__init__(*args, **kwargs)
def render(self, form, form_style, context):
html = ''
for field in self.fields:
result = self.detail.get_field_result(field)
field = {'auto_id': field} #: 设置 field id
html += loader.render_to_string(
self.template, {'field': field, 'result': result})
return html
[docs]class ModelFormAdminView(ModelAdminView):
"""
用于添加或修改数据的 AdminView,该类是一个基类,提供了数据表单显示及修改等通用功能,被 :class:`CreateAdminView` 及 :class:`UpdateAdminView` 继承
**Option 属性**
.. autoattribute:: form
.. autoattribute:: formfield_overrides
.. autoattribute:: readonly_fields
.. autoattribute:: style_fields
.. autoattribute:: relfield_style
.. autoattribute:: save_as
.. autoattribute:: save_on_top
.. autoattribute:: add_form_template
.. autoattribute:: change_form_template
.. autoattribute:: form_layout
"""
form = forms.ModelForm #: 使用 Model 生成 Form 的基本 Form 类,默认为 django.forms.ModelForm
formfield_overrides = {}
"""
可以指定某种类型的 DB Field,使用指定的 Form Field 属性,例如::
class AtricleAdmin(object):
formfield_overrides = {
models.FileField:{'widget': mywidgets.XFileWidget},
}
这样,显示所有 FileField 字段时,都会使用 ``mywidgets.XFileWidget`` 来显示
"""
readonly_fields = () #: 只读的字段,这些字段不能被编辑
style_fields = {}
"""
指定 Field 的 Style, Style一般用来实现同一种类型的字段的不同效果,例如同样是 radio button,有普通及``inline``两种 Style。
通常 xadmin 针对表单的插件会实现更多的 Field Style。您使用这些插件后,只要方便的将想要使用插件效果的字段设置成插件实现的 Style 即可,例如::
class AtricleAdmin(object):
style_fields = {"content": "rich-textarea"}
``rich-textarea`` 可能是某插件提供的 Style,这样显示 ``content`` 字段时就会使用该插件的效果了
"""
exclude = None
relfield_style = None #: 当 Model 是其他 Model 的 ref model 时,其他 Model 在显示本 Model 的字段时使用的 Field Style
save_as = False #: 是否显示 ``另存为`` 按钮
save_on_top = False #: 是否在页面上面显示按钮组
add_form_template = None #: 添加页面的模板
change_form_template = None #: 修改页面的模板
form_layout = None
"""
页面 Form 的 Layout 对象,是一个标准的 Crispy Form Layout 对象。使用 Layout 可以方便的定义整个 Form 页面的结构。
有关 Crispy Form 可以参考其文档 `Crispy Form 文档 <http://django-crispy-forms.readthedocs.org/en/latest/layouts.html>`_
设置 form_layout 的示例::
from xadmin.layout import Main, Side, Fieldset, Row, AppendedText
class AtricleAdmin(object):
form_layout = (
Main(
Fieldset('Comm data',
'title', 'category'
),
Inline(Log),
Fieldset('Details',
'short_title',
Row(AppendedText('file_size', 'MB'), 'author'),
'content'
),
),
Side(
Fieldset('Status',
'status',
),
)
)
有关 Layout 中元素的信息,可以参看文档 :ref:`form_layout`
"""
def __init__(self, request, *args, **kwargs):
overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
# 将 :attr:`formfield_overrides` 替换系统默认值
overrides.update(self.formfield_overrides)
self.formfield_overrides = overrides
super(ModelFormAdminView, self).__init__(request, *args, **kwargs)
@filter_hook
[docs] def formfield_for_dbfield(self, db_field, **kwargs):
"""
生成表单时的回调方法,返回 Form Field。
:param db_field: Model 的 DB Field
"""
# 如果使用了非自动生成的 intermediary model 则不显示该字段
if isinstance(db_field, models.ManyToManyField) and not db_field.rel.through._meta.auto_created:
return None
attrs = self.get_field_attrs(db_field, **kwargs)
return db_field.formfield(**dict(attrs, **kwargs))
@filter_hook
[docs] def get_field_style(self, db_field, style, **kwargs):
"""
根据 Field Style 返回 Form Field 属性。扩展插件可以过滤该方法,提供各种不同的 Style
:param db_field: Model 的 DB Field
:param style: 配置的 Field Style,该值来自于属性 :attr:`style_fields`
"""
if style in ('radio', 'radio-inline') and (db_field.choices or isinstance(db_field, models.ForeignKey)):
# fk 字段生成 radio 表单控件
attrs = {'widget': widgets.AdminRadioSelect(
attrs={'inline': style == 'radio-inline'})}
if db_field.choices:
attrs['choices'] = db_field.get_choices(
include_blank=db_field.blank,
blank_choice=[('', _('Null'))]
)
return attrs
if style in ('checkbox', 'checkbox-inline') and isinstance(db_field, models.ManyToManyField):
return {'widget': widgets.AdminCheckboxSelect(attrs={'inline': style == 'checkbox-inline'}),
'help_text': None}
@filter_hook
[docs] def get_field_attrs(self, db_field, **kwargs):
"""
根据 DB Field 返回 Form Field 的属性,dict类型。
:param db_field: Model 的 DB Field
"""
if db_field.name in self.style_fields:
# 如果设置了 Field Style,则返回 Style 的属性
attrs = self.get_field_style(
db_field, self.style_fields[db_field.name], **kwargs)
if attrs:
return attrs
if hasattr(db_field, "rel") and db_field.rel:
related_modeladmin = self.admin_site._registry.get(db_field.rel.to)
# 如果字段是关联字段,并且关联字段的 ModelAdmin 设置了 :attr:`relfield_style` 属性,则使用该值作为 Field Style
if related_modeladmin and hasattr(related_modeladmin, 'relfield_style'):
attrs = self.get_field_style(
db_field, related_modeladmin.relfield_style, **kwargs)
if attrs:
return attrs
if db_field.choices:
return {'widget': widgets.AdminSelectWidget}
for klass in db_field.__class__.mro():
# 根据 DB Field 的类,获取 Field 属性
if klass in self.formfield_overrides:
return self.formfield_overrides[klass].copy()
return {}
@filter_hook
[docs] def prepare_form(self):
"""
准备 Form,即调用 :meth:`get_model_form` 获取 form ,然后赋值给 :attr:`model_form` 属性
"""
self.model_form = self.get_model_form()
@filter_hook
[docs] def instance_forms(self):
"""
实例化 Form 对象,即使用 :meth:`get_form_datas` 返回的值初始化 Form,实例化的 Form 对象赋值为 :attr:`form_obj` 属性
"""
self.form_obj = self.model_form(**self.get_form_datas())
[docs] def setup_forms(self):
"""
配置 Form。主要是
"""
helper = self.get_form_helper()
if helper:
self.form_obj.helper = helper
@filter_hook
@filter_hook
[docs] def get_model_form(self, **kwargs):
"""
根据 Model 返回 Form 类,用来显示表单。
"""
if self.exclude is None:
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(self.get_readonly_fields())
if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
# 如果 :attr:`~xadmin.views.base.ModelAdminView.exclude` 是 None,并且 form 的 Meta.exclude 不为空,
# 则使用 form 的 Meta.exclude
exclude.extend(self.form._meta.exclude)
# 如果 exclude 是空列表,那么就设为 None
exclude = exclude or None
defaults = {
"form": self.form,
"fields": self.fields and list(self.fields) or None,
"exclude": exclude,
"formfield_callback": self.formfield_for_dbfield, # 设置生成表单字段的回调函数
}
defaults.update(kwargs)
# 使用 modelform_factory 生成 Form 类
return modelform_factory(self.model, **defaults)
@filter_hook
[docs] def get_form_layout(self):
"""
返回 Form Layout ,如果您设置了 :attr:`form_layout` 属性,则使用该属性,否则该方法会自动生成 Form Layout 。
有关 Form Layout 的更多信息可以参看 `Crispy Form 文档 <http://django-crispy-forms.readthedocs.org/en/latest/layouts.html>`_
设置 Form Layout 可以非常灵活的显示表单页面的各个元素
"""
layout = copy.deepcopy(self.form_layout)
fields = self.form_obj.fields.keys() + list(self.get_readonly_fields())
if layout is None:
layout = Layout(Container(Col('full',
Fieldset("", *fields, css_class="unsort no_title"), horizontal=True, span=12)
))
elif type(layout) in (list, tuple) and len(layout) > 0:
# 如果设置的 layout 是一个列表,那么按以下方法生成
if isinstance(layout[0], Column):
fs = layout
elif isinstance(layout[0], (Fieldset, TabHolder)):
fs = (Col('full', *layout, horizontal=True, span=12),)
else:
fs = (Col('full', Fieldset("", *layout, css_class="unsort no_title"), horizontal=True, span=12),)
layout = Layout(Container(*fs))
rendered_fields = [i[1] for i in layout.get_field_names()]
container = layout[0].fields
other_fieldset = Fieldset(_(u'Other Fields'), *[f for f in fields if f not in rendered_fields])
# 将所有没有显示的字段和在一个 Fieldset 里面显示
if len(other_fieldset.fields):
if len(container) and isinstance(container[0], Column):
# 把其他字段放在第一列显示
container[0].fields.append(other_fieldset)
else:
container.append(other_fieldset)
return layout
@filter_hook
[docs] def get_form_helper(self):
"""
取得 Crispy Form 需要的 FormHelper。具体信息可以参看 `Crispy Form 文档 <http://django-crispy-forms.readthedocs.org/en/latest/tags.html#crispy-tag>`_
"""
helper = FormHelper()
helper.form_tag = False # 默认不需要 crispy 生成 form_tag
helper.add_layout(self.get_form_layout())
# 处理只读字段
readonly_fields = self.get_readonly_fields()
if readonly_fields:
# 使用 :class:`xadmin.views.detail.DetailAdminUtil` 来显示只读字段的内容
detail = self.get_model_view(
DetailAdminUtil, self.model, self.form_obj.instance)
for field in readonly_fields:
# 替换只读字段
helper[field].wrap(ReadOnlyField, detail=detail)
return helper
@filter_hook
[docs] def get_readonly_fields(self):
"""
返回只读字段,子类或 OptionClass 可以复写该方法
"""
return self.readonly_fields
@filter_hook
[docs] def save_forms(self):
"""
保存表单,赋值为 :attr:`new_obj` 属性,这时该对象还没有保存到数据库中,也没有 pk 生成
"""
self.new_obj = self.form_obj.save(commit=False)
@filter_hook
@filter_hook
@csrf_protect_m
@filter_hook
[docs] def get(self, request, *args, **kwargs):
"""
显示表单。具体的程序执行流程为:
1. :meth:`prepare_form`
2. :meth:`instance_forms`
2.1 :meth:`get_form_datas`
3. :meth:`setup_forms`
4. :meth:`get_response`
"""
self.instance_forms()
self.setup_forms()
return self.get_response()
@csrf_protect_m
@transaction.commit_on_success
@filter_hook
[docs] def post(self, request, *args, **kwargs):
"""
保存表单数据。具体的程序执行流程为:
1. :meth:`prepare_form`
2. :meth:`instance_forms`
2.1 :meth:`get_form_datas`
3. :meth:`setup_forms`
4. :meth:`valid_forms`
4.1 :meth:`save_forms`
4.2 :meth:`save_models`
4.3 :meth:`save_related`
4.4 :meth:`post_response`
"""
self.instance_forms()
self.setup_forms()
if self.valid_forms():
self.save_forms()
self.save_models()
self.save_related()
response = self.post_response()
if isinstance(response, basestring):
return HttpResponseRedirect(response)
else:
return response
return self.get_response()
@filter_hook
[docs] def get_context(self):
"""
**Context Params**:
``form`` : Form 对象
``original`` : 要修改的原始数据对象
``show_delete`` : 是否显示删除项
``add`` : 是否是添加数据
``change`` : 是否是修改数据
``errors`` : Form 错误信息
"""
add = self.org_obj is None
change = self.org_obj is not None
new_context = {
'form': self.form_obj,
'original': self.org_obj,
'show_delete': self.org_obj is not None,
'add': add,
'change': change,
'errors': self.get_error_list(),
'has_add_permission': self.has_add_permission(),
'has_view_permission': self.has_view_permission(),
'has_change_permission': self.has_change_permission(self.org_obj),
'has_delete_permission': self.has_delete_permission(self.org_obj),
'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
'form_url': '',
'content_type_id': ContentType.objects.get_for_model(self.model).id,
'save_as': self.save_as,
'save_on_top': self.save_on_top,
}
# for submit line
new_context.update({
'onclick_attrib': '',
'show_delete_link': (new_context['has_delete_permission']
and (change or new_context['show_delete'])),
'show_save_as_new': change and self.save_as,
'show_save_and_add_another': new_context['has_add_permission'] and
(not self.save_as or add),
'show_save_and_continue': new_context['has_change_permission'],
'show_save': True
})
if self.org_obj and new_context['show_delete_link']:
new_context['delete_url'] = self.model_admin_url(
'delete', self.org_obj.pk)
context = super(ModelFormAdminView, self).get_context()
context.update(new_context)
return context
@filter_hook
[docs] def get_error_list(self):
"""
获取表单的错误信息列表。
"""
errors = forms.util.ErrorList()
if self.form_obj.is_bound:
errors.extend(self.form_obj.errors.values())
return errors
@filter_hook
[docs] def get_media(self):
return super(ModelFormAdminView, self).get_media() + self.form_obj.media + \
self.vendor('xadmin.page.form.js', 'xadmin.form.css')
[docs]class CreateAdminView(ModelFormAdminView):
"""
创建数据的 ModeAdminView 继承自 :class:`ModelFormAdminView` ,用于创建数据。
"""
def init_request(self, *args, **kwargs):
self.org_obj = None
if not self.has_add_permission():
raise PermissionDenied
# comm method for both get and post
self.prepare_form()
@filter_hook
[docs] def get_form_datas(self):
"""
从 Request 中返回 Form 的初始化数据
"""
if self.request_method == 'get':
initial = dict(self.request.GET.items())
for k in initial:
try:
f = self.opts.get_field(k)
except models.FieldDoesNotExist:
continue
if isinstance(f, models.ManyToManyField):
# 如果是多对多的字段,则使用逗号分割
initial[k] = initial[k].split(",")
return {'initial': initial}
else:
return {'data': self.request.POST, 'files': self.request.FILES}
@filter_hook
[docs] def get_context(self):
"""
**Context Params**:
``title`` : 表单标题
"""
new_context = {
'title': _('Add %s') % force_unicode(self.opts.verbose_name),
}
context = super(CreateAdminView, self).get_context()
context.update(new_context)
return context
@filter_hook
[docs] def get_breadcrumb(self):
bcs = super(ModelFormAdminView, self).get_breadcrumb()
item = {'title': _('Add %s') % force_unicode(self.opts.verbose_name)}
if self.has_add_permission():
item['url'] = self.model_admin_url('add')
bcs.append(item)
return bcs
@filter_hook
[docs] def get_response(self):
"""
返回显示表单页面的 Response ,子类或是 OptionClass 可以复写该方法
"""
context = self.get_context()
context.update(self.kwargs or {})
return TemplateResponse(
self.request, self.add_form_template or self.get_template_list(
'views/model_form.html'),
context, current_app=self.admin_site.name)
@filter_hook
[docs] def post_response(self):
"""
当成功保存数据后,会调用该方法返回 HttpResponse 或跳转地址
"""
request = self.request
msg = _(
'The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(self.opts.verbose_name),
'obj': "<a class='alert-link' href='%s'>%s</a>" % (self.model_admin_url('change', self.new_obj._get_pk_val()), force_unicode(self.new_obj))}
if "_continue" in request.REQUEST:
self.message_user(
msg + ' ' + _("You may edit it again below."), 'success')
# 继续编辑
return self.model_admin_url('change', self.new_obj._get_pk_val())
if "_addanother" in request.REQUEST:
self.message_user(msg + ' ' + (_("You may add another %s below.") % force_unicode(self.opts.verbose_name)), 'success')
# 返回添加页面添加另外一个
return request.path
else:
self.message_user(msg, 'success')
# 如果没有查看列表的权限就跳转到主页
if "_redirect" in request.REQUEST:
return request.REQUEST["_redirect"]
elif self.has_view_permission():
return self.model_admin_url('changelist')
else:
return self.get_admin_url('index')
[docs]class UpdateAdminView(ModelFormAdminView):
"""
修改数据的 ModeAdminView 继承自 :class:`ModelFormAdminView` ,用于修改数据。
"""
def init_request(self, object_id, *args, **kwargs):
self.org_obj = self.get_object(unquote(object_id))
if not self.has_change_permission(self.org_obj):
raise PermissionDenied
if self.org_obj is None:
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') %
{'name': force_unicode(self.opts.verbose_name), 'key': escape(object_id)})
# comm method for both get and post
self.prepare_form()
@filter_hook
[docs] def get_form_datas(self):
"""
获取 Form 数据
"""
params = {'instance': self.org_obj}
if self.request_method == 'post':
params.update(
{'data': self.request.POST, 'files': self.request.FILES})
return params
@filter_hook
[docs] def get_context(self):
"""
**Context Params**:
``title`` : 表单标题
``object_id`` : 修改的数据对象的 id
"""
new_context = {
'title': _('Change %s') % force_unicode(self.org_obj),
'object_id': str(self.org_obj.pk),
}
context = super(UpdateAdminView, self).get_context()
context.update(new_context)
return context
@filter_hook
[docs] def get_breadcrumb(self):
bcs = super(ModelFormAdminView, self).get_breadcrumb()
item = {'title': force_unicode(self.org_obj)}
if self.has_change_permission():
item['url'] = self.model_admin_url('change', self.org_obj.pk)
bcs.append(item)
return bcs
@filter_hook
[docs] def get_response(self, *args, **kwargs):
context = self.get_context()
context.update(kwargs or {})
return TemplateResponse(
self.request, self.change_form_template or self.get_template_list(
'views/model_form.html'),
context, current_app=self.admin_site.name)
def post(self, request, *args, **kwargs):
if "_saveasnew" in self.request.REQUEST:
return self.get_model_view(CreateAdminView, self.model).post(request)
return super(UpdateAdminView, self).post(request, *args, **kwargs)
@filter_hook
[docs] def post_response(self):
"""
当成功修改数据后,会调用该方法返回 HttpResponse 或跳转地址
"""
opts = self.new_obj._meta
obj = self.new_obj
request = self.request
verbose_name = opts.verbose_name
pk_value = obj._get_pk_val()
msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name':
force_unicode(verbose_name), 'obj': force_unicode(obj)}
if "_continue" in request.REQUEST:
self.message_user(
msg + ' ' + _("You may edit it again below."), 'success')
# 返回原页面继续编辑
return request.path
elif "_addanother" in request.REQUEST:
self.message_user(msg + ' ' + (_("You may add another %s below.")
% force_unicode(verbose_name)), 'success')
return self.model_admin_url('add')
else:
self.message_user(msg, 'success')
# 如果没有查看列表的权限就跳转到主页
if "_redirect" in request.REQUEST:
return request.REQUEST["_redirect"]
elif self.has_view_permission():
change_list_url = self.model_admin_url('changelist')
if 'LIST_QUERY' in self.request.session \
and self.request.session['LIST_QUERY'][0] == self.model_info:
change_list_url += '?' + self.request.session['LIST_QUERY'][1]
return change_list_url
else:
return self.get_admin_url('index')
class ModelFormAdminUtil(ModelFormAdminView):
"""
工具类,主要用于在其他页面显示表单字段,用于 editable 插件中,使用示例::
def some_func(self):
edit_view = self.get_model_view(ModelFormAdminUtil, self.model, obj)
form = edit_view.form_obj
"""
def init_request(self, obj=None):
self.org_obj = obj
self.prepare_form()
self.instance_forms()
@filter_hook
def get_form_datas(self):
return {'instance': self.org_obj}