将 ArrayField 替换为 django-admin 中其他模型的几个字段

Replace ArrayField for several fields from other models in django-admin

我想将模型字段存储为其他原子模型的外键数组。它使模型更加灵活:我可以 add/remove/inherit(如果它是树节点)任何预定义的属性而无需编程。

让我们从原子模型开始。它包含一些机密属性。

class Liquid(models.Model):
    volume = models.DecimalField(max_digits=12, decimal_places=6)

class Granular(models.Model):
    weight = models.DecimalField(max_digits=16, decimal_places=9)

class Serial(models.Model):
    lot_number = models.CharField(max_length=16)
    estimated_release_time = models.DateTimeField()
    deadline_time = models.DateTimeField())

我想模拟以下代码。无效,因为:
1. 我想存储几个模型的外键(GenericForeignKey 可以帮忙);
2. ArrayField暂不支持外键数组;

class Entity(models.Model):
        arr = ArrayField(models.ForeignKey(DifferentAtomicModels))

仿真如下。其中even numbers是django.contrib.contenttypes.models.ContentType的pk,odd numbers是atomic-Model的pk(atomic-model是从前面偶数得到的)。

class Entity(models.Model):
    arr = ArrayField(models.IntegerField(), null=True, blank=True)

好吧,在 django-admin changelist_view 和 changeform_view 我想用从它获得的另一个模型的字段替换 arr 字段。

实现这个的方法是什么?哪里最好干预 django-admin 行为(覆盖表单域,制作自定义小部件,smth。其他......)?现在我正在研究重写 ModelAdmin -> changeform_view、get_form、modelform_factory;但是代码中有很多relations/inheritions,我有点困惑......内联没有帮助,因为没有真正的外键。

好吧,我将展示我实现此目标的有点肮脏的方法。我有 overrided/created changeform_view of admin.ModelAdmin 和对应的 functions/templates: get_rel_fieldsets, change_form.html 可能还有别的... 下面的代码是为与类别一起使用而设计的,它可以从祖先那里继承一些参数(显示为 ro)并指定自己的参数(显示为 rw)。

欢迎任何评论或更正,请注意:我是 python/django 的新手。

# myapp/models/py
from django.contrib.postgres.fields import ArrayField
class MultiRelationalArrayField(ArrayField):
    pass

class SomeAtomicModel(models.Model):
    someproperty = models.CharField(max_length=16)
    someproperty2 = ...
    someproperty3 = ...

    def modeladmin_params(self):
        '''ModelAdmin parameters for atomicmodels can optionaly be specified in this method'''
        # Both 'fields' and 'readonly_fields' must exist!
        return {
            'fields': [('someproperty', 'someproperty2'), 'someproperty3')],
            'readonly_fields': [],
        }

class Category(MPTTModel):
    parent = TreeForeignKey('self', null=True, blank=True,
        verbose_name=_("parent"), related_name='children')
    name = models.CharField(_("name"), max_length=64)
    relations = MultiRelationalArrayField(models.PositiveIntegerField(), null=True, blank=True,
        help_text=_("List of even:ContentType.id, odd:object.id. It's store relations to various predefined property sets."))

    def get_relations(self):
        relations = self.relations or []
        return relations

    def get_ancestors_relations(self):
        '''Return list of relations with relations of all ancestors starts from root'''
        relations = []
        for ct in self.get_ancestors():
            relations.extend(ct.get_relations())
        return relations

    def get_objects_from_relations(self, rel_list=None):
        '''
        Return list of objects mentioned in self.get_relations() (by default).
        If rel_list specified return list of objects from it.
        '''
        objects = []
        if rel_list is None:
            rel = self.get_relations()
            if rel == []:
                return []
            rel_list = rel
        if len(rel_list) % 2:
            # raise ValueError('List length must be even, NOT odd.')  # Doesn't work! Dont' know why
            raise RuntimeError('List length must be even, NOT odd.')
        for i in range(0, len(rel_list)-1, 2):
            objects.append(ContentType.objects.get_for_id(rel_list[i]).get_object_for_this_type(pk=rel_list[i+1]))
        return objects

    @csrf_protect_m
    @transaction.atomic
    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        '''Customization for MultiRelationalArrayField support'''
        # Start main part of customization
        add = object_id is None
        all_rform_validated = True
        if self.rel_array_fields and not add:   # Fiedls from self.rel_array_fields will not be shown on obj addition
            to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
            # ?TODO: to_field check for rel objs
            obj = self.get_object(request, unquote(object_id), to_field)
            anc_rel = obj.get_ancestors_relations()
            rel_anc_objs = obj.get_objects_from_relations(anc_rel)
            rel_objs = obj.get_objects_from_relations()
            rel_all_objs = rel_anc_objs + rel_objs
            # rel_anc_objs will be readonly
            anc_rel_len2 = anc_rel.__len__()//2
            if request.method == 'POST' and "_saveasnew" in request.POST:
                if rel_all_objs != []:
                    return HttpResponseNotAllowed(_("'_saveasnew' option is prohibited for objects with realtions in array field."))

            # Generate ModelAdmin & ModelForm for rel_all_objs on the fly
            # rel_O_A_F = [(rel_obj instance, rel_obj_ModelAdmin instance, rel_obj_ModelForm class), ...]
            rel_O_A_F = []
            for i, o in enumerate(rel_all_objs):
                rNAME = o._meta.model.__name__ + 'RelAdmin'
                # To avoid batch ModelAdmin creations we can set ModelAdmin parameters in modeladmin_params() obj.method
                if hasattr(o, 'modeladmin_params'):
                    rparams = o.modeladmin_params()
                    rfields = rparams['fields']
                    rreadonly_fields = rparams['readonly_fields']
                else:
                    rfields = None
                    rreadonly_fields = None
                rATTRS = {
                    'formfield_overrides': self.formfield_overrides,
                    'readonly_fields': rreadonly_fields or (),
                }

                # Separation of parameters from ancestors and object parameters
                if not anc_rel_len2:  # No relations from ancestors
                    rATTRS.update({
                        'fieldsets': self.get_rel_fieldsets(request, o, False if not i else True, 1, **{'fields': rfields}),
                    })
                elif i in range(0, anc_rel_len2):  # Relations from ancestors, all will be readonly
                    rfieldsets = self.get_rel_fieldsets(request, o, False if not i else True, 0, **{'fields': rfields})
                    rATTRS.update({
                        'fieldsets': rfieldsets,
                        'readonly_fields': flatten_fieldsets(rfieldsets),
                    })
                elif i == anc_rel_len2:  # The first relation of obj (rw)
                    rfieldsets = self.get_rel_fieldsets(request, o, False, 1, **{'fields': rfields})
                    rATTRS.update({
                        'fieldsets': rfieldsets,
                    })
                else:  # Next relations of obj (rw)
                    if rfields:
                        rATTRS.update({
                            'fieldsets': [(None, {'fields': rfields})],
                        })

                rADM = type(rNAME, (admin.ModelAdmin,), rATTRS)(o._meta.model, self.admin_site)
                rel_O_A_F.append((o, rADM, rADM.get_form(request, o)))

            # Generate adminFroms for related objects
            extra_context = extra_context or {}
            extra_context['rel_anc_adminforms'] = []
            extra_context['rel_adminforms'] = []
            extra_context['media'] = []
            errlst = []
            rmedia = self.media
            for i, (robj, radm, rForm) in enumerate(rel_O_A_F):
                if request.method == 'POST':
                    rform = rForm(request.POST, request.FILES, instance=robj)
                    if not rform.is_valid():
                        all_rform_validated = False
                    errlst.extend(helpers.AdminErrorList(rform, []))
                else:
                    rform = rForm(instance=robj)

                radminForm = helpers.AdminForm(
                    rform,
                    list(radm.get_fieldsets(request, robj)),
                    radm.get_prepopulated_fields(request, robj),
                    radm.get_readonly_fields(request, robj),
                    model_admin=radm
                )

                if i < anc_rel_len2:
                    extra_context['rel_anc_adminforms'].append(radminForm)
                else:
                    extra_context['rel_adminforms'].append(radminForm)
                rmedia = rmedia + radminForm.media
            extra_context.update({'media': rmedia})
            if errlst:
                extra_context.update({'errors': errlst})

        # return super(ModelAdmin, self).changeform_view(request, object_id, form_url, extra_context=extra_context)
        # Interrupt main part of customization. Then some inserts to official code of django 1.8 (see '# custom' at the end fo line)

        to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
        if to_field and not self.to_field_allowed(request, to_field):
            raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)

        model = self.model
        opts = model._meta
        add = object_id is None

        if add:
            if not self.has_add_permission(request):
                raise PermissionDenied
            obj = None

        else:
            obj = self.get_object(request, unquote(object_id), to_field)

            if not self.has_change_permission(request, obj):
                raise PermissionDenied

            if obj is None:
                raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
                    'name': force_text(opts.verbose_name), 'key': escape(object_id)})

            if request.method == 'POST' and "_saveasnew" in request.POST:
                return self.add_view(request, form_url=reverse('admin:%s_%s_add' % (
                    opts.app_label, opts.model_name),
                    current_app=self.admin_site.name))

        ModelForm = self.get_form(request, obj)
        if request.method == 'POST':
            form = ModelForm(request.POST, request.FILES, instance=obj)
            if form.is_valid():
                form_validated = True
                new_object = self.save_form(request, form, change=not add)
            else:
                form_validated = False
                new_object = form.instance
            formsets, inline_instances = self._create_formsets(request, new_object, change=not add)

            # if all_valid(formsets) and form_validated:  # official
            # Also check MultiRelationalArrayField models forms validation  # custom
            if all_valid(formsets) and form_validated and all_rform_validated:  # custom
                self.save_model(request, new_object, form, not add)
                self.save_related(request, form, formsets, not add)

                if add:
                    self.log_addition(request, new_object)
                    return self.response_add(request, new_object)
                else:
                    # Also save models from MultiRelationalArrayField  # custom
                    if self.rel_array_fields:  # custom
                        for robj, radm, rForm in rel_O_A_F:  # custom
                            rform = rForm(request.POST, request.FILES, instance=robj)  # custom
                            rnew_object = radm.save_form(request, rform, change=not add)  # custom
                            radm.save_model(request, rnew_object, rform, not add)  # custom

                    change_message = self.construct_change_message(request, form, formsets)
                    self.log_change(request, new_object, change_message)
                    return self.response_change(request, new_object)
        else:
            if add:
                initial = self.get_changeform_initial_data(request)
                form = ModelForm(initial=initial)
                formsets, inline_instances = self._create_formsets(request, self.model(), change=False)
            else:
                form = ModelForm(instance=obj)
                formsets, inline_instances = self._create_formsets(request, obj, change=True)

        adminForm = helpers.AdminForm(
            form,
            list(self.get_fieldsets(request, obj)),
            self.get_prepopulated_fields(request, obj),
            self.get_readonly_fields(request, obj),
            model_admin=self)
        media = self.media + adminForm.media

        inline_formsets = self.get_inline_formsets(request, formsets, inline_instances, obj)
        for inline_formset in inline_formsets:
            media = media + inline_formset.media

        context = dict(self.admin_site.each_context(request),
            title=(_('Add %s') if add else _('Change %s')) % force_text(opts.verbose_name),
            adminform=adminForm,
            object_id=object_id,
            original=obj,
            is_popup=(IS_POPUP_VAR in request.POST or
                      IS_POPUP_VAR in request.GET),
            to_field=to_field,
            media=media,
            inline_admin_formsets=inline_formsets,
            errors=helpers.AdminErrorList(form, formsets),
            preserved_filters=self.get_preserved_filters(request),
        )

        # Add MultiRelationalArrayField models forms errors to main form errors  # custom
        try:  # custom
            extra_context['errors'].extend(context['errors'])  # custom
        except:  # custom
            pass  # custom
        try:  # custom
            extra_context['media'] = context['media'] + extra_context['media']  # custom
        except:  # custom
            pass  # custom

        context.update(extra_context or {})
        return self.render_change_form(request, context, add=add, change=not add, obj=obj, form_url=form_url)


# myapp/admin.py
from django.contrib import admin
from django.forms.models import fields_for_model
from django.contrib.admin.options import (csrf_protect_m, transaction, TO_FIELD_VAR, DisallowedModelAdminToField, PermissionDenied, Http404, unquote, force_text, reverse, escape, all_valid, helpers, IS_POPUP_VAR, flatten_fieldsets, partial, forms, FieldError, modelform_defines_fields, modelform_factory)
class CategoryAdmin(TreeEditor, ModelAdmin)::
    rel_array_fields = ['get_ancestors_relations', 'relations']
    readonly_rel_array_fields = ['get_ancestors_relations']
    rel_fieldsets = [
        [_('Parameters inherited from ancestors'), {
            'fields': ['get_ancestors_relations', ],
            # 'classes': ('collapse', ),
            'description': _("They also will be inherited by descendants. You can change them only via changing ancestors parameters (higher levels)."),
        }],
        [_('Parameters'), {
            'fields': ['relations', ],
            # 'classes': ('collapse', ),
            'description': _('They will be added to inherition.'),
        }],
    ]
    change_form_template = 'myapp/change_form.html'


    def get_rel_fieldsets(self, request, obj, fields_only, fieldset_index=1, **kwargs):
        if 'fields' in kwargs and kwargs['fields'] is not None:
            fields = kwargs['fields']
        else:
            fields = list(fields_for_model(obj._meta.model).keys())

        if fields_only:
            return [(None, {'fields': fields})]
        else:
            fieldset = self.rel_fieldsets[fieldset_index]
            fieldset[1]['fields'] = fields
            return [fieldset]

# myapp/templates/myapp/change_form.html
{% extends "admin/change_form.html" %}

{% block field_sets %}
{{ block.super }}
{% if rel_anc_adminforms %}
    {% for adminform in rel_anc_adminforms %}
        {% for fieldset in adminform %}
            {% if fieldset.name %}
            <fieldset class="module aligned {{ fieldset.classes }}">
                <h2>{{ fieldset.name }}</h2>
                {% if fieldset.description %}
                    <div class="description">{{ fieldset.description|safe }}</div>
                {% endif %}
            {% endif %}
            {% for line in fieldset %}
                {% include "biokit/includes/line.html" %}
            {% endfor %}
        {% endfor %}
    {% endfor %}
            </fieldset>
{% endif %}
{% if rel_adminforms %}
    {% for adminform in rel_adminforms %}
        THE_SAME_AS_ABOVE
    {% endfor %}
            </fieldset>
{% endif %}
{% endblock %}