Source code for xadmin.views.edit

from __future__ import absolute_import
import copy

from crispy_forms.utils import TEMPLATE_PACK
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, FieldError
from django.db import models, transaction
from django.forms.models import modelform_factory, modelform_defines_fields
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.utils import six
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils.text import capfirst, get_text_list
from django.template import loader
from django.utils.translation import ugettext as _
from django.forms.widgets import Media
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, template_pack=TEMPLATE_PACK, **kwargs):
        html = ''
        for field in self.fields:
            result = self.detail.get_field_result(field)
            field = {'auto_id': field}
            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)
[docs] @filter_hook def formfield_for_dbfield(self, db_field, **kwargs): """ 生成表单时的回调方法,返回 Form Field。 :param db_field: Model 的 DB Field """ # 如果使用了非自动生成的 intermediary model 则不显示该字段 # If it uses an intermediary model that isn't auto created, don't show # a field in admin. if isinstance(db_field, models.ManyToManyField) and not db_field.remote_field.through._meta.auto_created: return None attrs = self.get_field_attrs(db_field, **kwargs) return db_field.formfield(**dict(attrs, **kwargs))
[docs] @filter_hook 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': 'inline' if style == 'radio-inline' else ''})} 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}
[docs] @filter_hook 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 {}
[docs] @filter_hook def prepare_form(self): """ 准备 Form,即调用 :meth:`get_model_form` 获取 form ,然后赋值给 :attr:`model_form` 属性 """ self.model_form = self.get_model_form()
[docs] @filter_hook 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
[docs] @filter_hook def valid_forms(self): """ 验证 Form 的数据合法性 """ return self.form_obj.is_valid()
[docs] @filter_hook 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) if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): defaults['fields'] = forms.ALL_FIELDS return modelform_factory(self.model, **defaults) try: # 使用 modelform_factory 生成 Form 类 return modelform_factory(self.model, **defaults) except FieldError as e: raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.' % (e, self.__class__.__name__))
[docs] @filter_hook 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) arr = self.form_obj.fields.keys() if six.PY3: arr = [k for k in arr] fields = arr + 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
[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.include_media = False 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
[docs] @filter_hook def get_readonly_fields(self): """ 返回只读字段,子类或 OptionClass 可以复写该方法 """ return self.readonly_fields
[docs] @filter_hook def save_forms(self): """ 保存表单,赋值为 :attr:`new_obj` 属性,这时该对象还没有保存到数据库中,也没有 pk 生成 """ self.new_obj = self.form_obj.save(commit=False)
[docs] @filter_hook def change_message(self): change_message = [] if self.org_obj is None: change_message.append(_('Added.')) elif self.form_obj.changed_data: change_message.append(_('Changed %s.') % get_text_list(self.form_obj.changed_data, _('and'))) change_message = ' '.join(change_message) return change_message or _('No fields changed.')
[docs] @filter_hook def save_models(self): """ 保存数据到数据库中 """ self.new_obj.save() flag = self.org_obj is None and 'create' or 'change' self.log(flag, self.change_message(), self.new_obj)
[docs] @csrf_protect_m @filter_hook 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()
[docs] @csrf_protect_m @transaction.atomic @filter_hook 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() cls_str = str if six.PY3 else basestring if isinstance(response, cls_str): return HttpResponseRedirect(response) else: return response return self.get_response()
[docs] @filter_hook 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
[docs] @filter_hook def get_error_list(self): """ 获取表单的错误信息列表。 """ errors = forms.utils.ErrorList() if self.form_obj.is_bound: errors.extend(self.form_obj.errors.values()) return errors
[docs] @filter_hook def get_media(self): try: m = self.form_obj.media except: m = Media() return super(ModelFormAdminView, self).get_media() + m + \ self.vendor('xadmin.page.form.js', 'xadmin.form.css')
[docs]class CreateAdminView(ModelFormAdminView): """ 创建数据的 ModeAdminView 继承自 :class:`ModelFormAdminView` ,用于创建数据。 """
[docs] 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()
[docs] @filter_hook def get_form_datas(self): """ 从 Request 中返回 Form 的初始化数据 """ # Prepare the dict of initial data from the request. # We have to special-case M2Ms as a list of comma-separated PKs. 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}
[docs] @filter_hook def get_context(self): """ **Context Params**: ``title`` : 表单标题 """ new_context = { 'title': _('Add %s') % force_text(self.opts.verbose_name), } context = super(CreateAdminView, self).get_context() context.update(new_context) return context
[docs] @filter_hook def get_breadcrumb(self): bcs = super(ModelFormAdminView, self).get_breadcrumb() item = {'title': _('Add %s') % force_text(self.opts.verbose_name)} if self.has_add_permission(): item['url'] = self.model_admin_url('add') bcs.append(item) return bcs
[docs] @filter_hook 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)
[docs] @filter_hook def post_response(self): """ 当成功保存数据后,会调用该方法返回 HttpResponse 或跳转地址 """ request = self.request msg = _( 'The %(name)s "%(obj)s" was added successfully.') % {'name': force_text(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_text(self.new_obj))} if "_continue" in request.POST: 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.POST: self.message_user(msg + ' ' + (_("You may add another %s below.") % force_text(self.opts.verbose_name)), 'success') # 返回添加页面添加另外一个 return request.path else: self.message_user(msg, 'success') # Figure out where to redirect. If the user has change permission, # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. # 如果没有查看列表的权限就跳转到主页 if "_redirect" in request.POST: return request.POST["_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` ,用于修改数据。 """
[docs] 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_text(self.opts.verbose_name), 'key': escape(object_id)}) # comm method for both get and post self.prepare_form()
[docs] @filter_hook 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
[docs] @filter_hook def get_context(self): """ **Context Params**: ``title`` : 表单标题 ``object_id`` : 修改的数据对象的 id """ new_context = { 'title': _('Change %s') % force_text(self.org_obj), 'object_id': str(self.org_obj.pk), } context = super(UpdateAdminView, self).get_context() context.update(new_context) return context
[docs] @filter_hook def get_breadcrumb(self): bcs = super(ModelFormAdminView, self).get_breadcrumb() item = {'title': force_text(self.org_obj)} if self.has_change_permission(): item['url'] = self.model_admin_url('change', self.org_obj.pk) bcs.append(item) return bcs
[docs] @filter_hook 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)
[docs] def post(self, request, *args, **kwargs): if "_saveasnew" in self.request.POST: return self.get_model_view(CreateAdminView, self.model).post(request) return super(UpdateAdminView, self).post(request, *args, **kwargs)
[docs] @filter_hook 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_text(verbose_name), 'obj': force_text(obj)} if "_continue" in request.POST: self.message_user( msg + ' ' + _("You may edit it again below."), 'success') # 返回原页面继续编辑 return request.path elif "_addanother" in request.POST: self.message_user(msg + ' ' + (_("You may add another %s below.") % force_text(verbose_name)), 'success') return self.model_admin_url('add') else: self.message_user(msg, 'success') # Figure out where to redirect. If the user has change permission, # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. # 如果没有查看列表的权限就跳转到主页 if "_redirect" in request.POST: return request.POST["_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}