import copy import inspect from django import forms from django.forms.formsets import all_valid, DELETION_FIELD_NAME from django.forms.models import inlineformset_factory, BaseInlineFormSet, modelform_defines_fields from django.contrib.contenttypes.forms import BaseGenericInlineFormSet, generic_inlineformset_factory from django.template import loader from django.template.loader import render_to_string from django.contrib.auth import get_permission_codename from django.utils import six from django.utils.encoding import smart_text from crispy_forms.utils import TEMPLATE_PACK from xadmin.layout import FormHelper, Layout, flatatt, Container, Column, Field, Fieldset from xadmin.plugins.utils import get_context_dict from xadmin.sites import site from xadmin.views import BaseAdminPlugin, ModelFormAdminView, DetailAdminView, filter_hook class ShowField(Field): template = "xadmin/layout/field_value.html" def __init__(self, admin_view, *args, **kwargs): super(ShowField, self).__init__(*args, **kwargs) self.admin_view = admin_view if admin_view.style == 'table': self.template = "xadmin/layout/field_value_td.html" def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs): html = '' detail = form.detail for field in self.fields: if not isinstance(form.fields[field].widget, forms.HiddenInput): result = detail.get_field_result(field) html += loader.render_to_string( self.template, context={'field': form[field], 'result': result}) return html class DeleteField(Field): def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs): if form.instance.pk: self.attrs['type'] = 'hidden' return super(DeleteField, self).render(form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs) else: return "" class TDField(Field): template = "xadmin/layout/td-field.html" class InlineStyleManager(object): inline_styles = {} def register_style(self, name, style): self.inline_styles[name] = style def get_style(self, name='stacked'): return self.inline_styles.get(name) style_manager = InlineStyleManager() class InlineStyle(object): template = 'xadmin/edit_inline/stacked.html' def __init__(self, view, formset): self.view = view self.formset = formset def update_layout(self, helper): pass def get_attrs(self): return {} style_manager.register_style('stacked', InlineStyle) class OneInlineStyle(InlineStyle): template = 'xadmin/edit_inline/one.html' style_manager.register_style("one", OneInlineStyle) class AccInlineStyle(InlineStyle): template = 'xadmin/edit_inline/accordion.html' style_manager.register_style("accordion", AccInlineStyle) class TabInlineStyle(InlineStyle): template = 'xadmin/edit_inline/tab.html' style_manager.register_style("tab", TabInlineStyle) class TableInlineStyle(InlineStyle): template = 'xadmin/edit_inline/tabular.html' def update_layout(self, helper): helper.add_layout( Layout(*[TDField(f) for f in self.formset[0].fields.keys()])) def get_attrs(self): fields = [] readonly_fields = [] if len(self.formset): fields = [f for k, f in self.formset[0].fields.items() if k != DELETION_FIELD_NAME] readonly_fields = [f for f in getattr(self.formset[0], 'readonly_fields', [])] return { 'fields': fields, 'readonly_fields': readonly_fields } style_manager.register_style("table", TableInlineStyle) def replace_field_to_value(layout, av): if layout: cls_str = str if six.PY3 else basestring for i, lo in enumerate(layout.fields): if isinstance(lo, Field) or issubclass(lo.__class__, Field): layout.fields[i] = ShowField(av, *lo.fields, **lo.attrs) elif isinstance(lo, cls_str): layout.fields[i] = ShowField(av, lo) elif hasattr(lo, 'get_field_names'): replace_field_to_value(lo, av) class InlineModelAdmin(ModelFormAdminView): fk_name = None formset = BaseInlineFormSet extra = 3 max_num = None can_delete = True fields = [] admin_view = None style = 'stacked' def init(self, admin_view): self.admin_view = admin_view self.parent_model = admin_view.model self.org_obj = getattr(admin_view, 'org_obj', None) self.model_instance = self.org_obj or admin_view.model() return self @filter_hook def get_formset(self, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" 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: # Take the custom ModelForm's Meta.exclude into account only if the # InlineModelAdmin doesn't define its own. exclude.extend(self.form._meta.exclude) # if exclude is an empty list we use None, since that's the actual # default exclude = exclude or None can_delete = self.can_delete and self.has_delete_permission() defaults = { "form": self.form, "formset": self.formset, "fk_name": self.fk_name, 'fields': forms.ALL_FIELDS, "exclude": exclude, "formfield_callback": self.formfield_for_dbfield, "extra": self.extra, "max_num": self.max_num, "can_delete": can_delete, } defaults.update(kwargs) return inlineformset_factory(self.parent_model, self.model, **defaults) @filter_hook def instance_form(self, **kwargs): formset = self.get_formset(**kwargs) attrs = { 'instance': self.model_instance, 'queryset': self.queryset() } if self.request_method == 'post': attrs.update({ 'data': self.request.POST, 'files': self.request.FILES, 'save_as_new': "_saveasnew" in self.request.POST }) instance = formset(**attrs) instance.view = self helper = FormHelper() helper.form_tag = False helper.include_media = False # override form method to prevent render csrf_token in inline forms, see template 'bootstrap/whole_uni_form.html' helper.form_method = 'get' style = style_manager.get_style( 'one' if self.max_num == 1 else self.style)(self, instance) style.name = self.style if len(instance): layout = copy.deepcopy(self.form_layout) if layout is None: layout = Layout(*instance[0].fields.keys()) elif type(layout) in (list, tuple) and len(layout) > 0: layout = Layout(*layout) rendered_fields = [i[1] for i in layout.get_field_names()] layout.extend([f for f in instance[0] .fields.keys() if f not in rendered_fields]) helper.add_layout(layout) style.update_layout(helper) # replace delete field with Dynamic field, for hidden delete field when instance is NEW. helper[DELETION_FIELD_NAME].wrap(DeleteField) instance.helper = helper instance.style = style readonly_fields = self.get_readonly_fields() if readonly_fields: for form in instance: form.readonly_fields = [] inst = form.save(commit=False) if inst: meta_field_names = [field.name for field in inst._meta.get_fields()] for readonly_field in readonly_fields: value = None label = None if readonly_field in meta_field_names: label = inst._meta.get_field(readonly_field).verbose_name value = smart_text(getattr(inst, readonly_field)) elif inspect.ismethod(getattr(inst, readonly_field, None)): value = getattr(inst, readonly_field)() label = getattr(getattr(inst, readonly_field), 'short_description', readonly_field) elif inspect.ismethod(getattr(self, readonly_field, None)): value = getattr(self, readonly_field)(inst) label = getattr(getattr(self, readonly_field), 'short_description', readonly_field) if value: form.readonly_fields.append({'label': label, 'contents': value}) return instance def has_auto_field(self, form): if form._meta.model._meta.has_auto_field: return True for parent in form._meta.model._meta.get_parent_list(): if parent._meta.has_auto_field: return True return False def queryset(self): queryset = super(InlineModelAdmin, self).queryset() if not self.has_change_permission() and not self.has_view_permission(): queryset = queryset.none() return queryset def has_add_permission(self): if self.opts.auto_created: return self.has_change_permission() codename = get_permission_codename('add', self.opts) return self.user.has_perm("%s.%s" % (self.opts.app_label, codename)) def has_change_permission(self): opts = self.opts if opts.auto_created: for field in opts.fields: if field.remote_field and field.remote_field.model != self.parent_model: opts = field.remote_field.model._meta break codename = get_permission_codename('change', opts) return self.user.has_perm("%s.%s" % (opts.app_label, codename)) def has_delete_permission(self): if self.opts.auto_created: return self.has_change_permission() codename = get_permission_codename('delete', self.opts) return self.user.has_perm("%s.%s" % (self.opts.app_label, codename)) class GenericInlineModelAdmin(InlineModelAdmin): ct_field = "content_type" ct_fk_field = "object_id" formset = BaseGenericInlineFormSet def get_formset(self, **kwargs): 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: # Take the custom ModelForm's Meta.exclude into account only if the # GenericInlineModelAdmin doesn't define its own. exclude.extend(self.form._meta.exclude) exclude = exclude or None can_delete = self.can_delete and self.has_delete_permission() defaults = { "ct_field": self.ct_field, "fk_field": self.ct_fk_field, "form": self.form, "formfield_callback": self.formfield_for_dbfield, "formset": self.formset, "extra": self.extra, "can_delete": can_delete, "can_order": False, "max_num": self.max_num, "exclude": exclude, 'fields': forms.ALL_FIELDS } defaults.update(kwargs) return generic_inlineformset_factory(self.model, **defaults) class InlineFormset(Fieldset): def __init__(self, formset, allow_blank=False, **kwargs): self.fields = [] self.css_class = kwargs.pop('css_class', '') self.css_id = "%s-group" % formset.prefix self.template = formset.style.template self.inline_style = formset.style.name if allow_blank and len(formset) == 0: self.template = 'xadmin/edit_inline/blank.html' self.inline_style = 'blank' self.formset = formset self.model = formset.model self.opts = formset.model._meta self.flat_attrs = flatatt(kwargs) self.extra_attrs = formset.style.get_attrs() def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs): context = get_context_dict(context) context.update(dict( formset=self, prefix=self.formset.prefix, inline_style=self.inline_style, **self.extra_attrs )) return render_to_string(self.template, context) class Inline(Fieldset): def __init__(self, rel_model): self.model = rel_model self.fields = [] super(Inline, self).__init__(legend="") def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs): return "" def get_first_field(layout, clz): for layout_object in layout.fields: if issubclass(layout_object.__class__, clz): return layout_object elif hasattr(layout_object, 'get_field_names'): gf = get_first_field(layout_object, clz) if gf: return gf def replace_inline_objects(layout, fs): if not fs: return for i, layout_object in enumerate(layout.fields): if isinstance(layout_object, Inline) and layout_object.model in fs: layout.fields[i] = fs.pop(layout_object.model) elif hasattr(layout_object, 'get_field_names'): replace_inline_objects(layout_object, fs) class InlineFormsetPlugin(BaseAdminPlugin): inlines = [] @property def inline_instances(self): if not hasattr(self, '_inline_instances'): inline_instances = [] for inline_class in self.inlines: inline = self.admin_view.get_view( (getattr(inline_class, 'generic_inline', False) and GenericInlineModelAdmin or InlineModelAdmin), inline_class).init(self.admin_view) if not (inline.has_add_permission() or inline.has_change_permission() or inline.has_delete_permission() or inline.has_view_permission()): continue if not inline.has_add_permission(): inline.max_num = 0 inline_instances.append(inline) self._inline_instances = inline_instances return self._inline_instances def instance_forms(self, ret): self.formsets = [] for inline in self.inline_instances: if inline.has_change_permission(): self.formsets.append(inline.instance_form()) else: self.formsets.append(self._get_detail_formset_instance(inline)) self.admin_view.formsets = self.formsets def valid_forms(self, result): return all_valid(self.formsets) and result def save_related(self): for formset in self.formsets: formset.instance = self.admin_view.new_obj formset.save() def get_context(self, context): context['inline_formsets'] = self.formsets return context def get_error_list(self, errors): for fs in self.formsets: errors.extend(fs.non_form_errors()) for errors_in_inline_form in fs.errors: errors.extend(errors_in_inline_form.values()) return errors def get_form_layout(self, layout): allow_blank = isinstance(self.admin_view, DetailAdminView) # fixed #176 bug, change dict to list fs = [(f.model, InlineFormset(f, allow_blank)) for f in self.formsets] replace_inline_objects(layout, fs) if fs: container = get_first_field(layout, Column) if not container: container = get_first_field(layout, Container) if not container: container = layout # fixed #176 bug, change dict to list for key, value in fs: container.append(value) return layout def get_media(self, media): for fs in self.formsets: media = media + fs.media if self.formsets: media = media + self.vendor( 'xadmin.plugin.formset.js', 'xadmin.plugin.formset.css') return media def _get_detail_formset_instance(self, inline): formset = inline.instance_form(extra=0, max_num=0, can_delete=0) formset.detail_page = True if True: replace_field_to_value(formset.helper.layout, inline) model = inline.model opts = model._meta fake_admin_class = type(str('%s%sFakeAdmin' % (opts.app_label, opts.model_name)), (object, ), {'model': model}) for form in formset.forms: instance = form.instance if instance.pk: form.detail = self.get_view( DetailAdminUtil, fake_admin_class, instance) return formset class DetailAdminUtil(DetailAdminView): def init_request(self, obj): self.obj = obj self.org_obj = obj class DetailInlineFormsetPlugin(InlineFormsetPlugin): def get_model_form(self, form, **kwargs): self.formsets = [self._get_detail_formset_instance( inline) for inline in self.inline_instances] return form site.register_plugin(InlineFormsetPlugin, ModelFormAdminView) site.register_plugin(DetailInlineFormsetPlugin, DetailAdminView)