How the django admin “change_view” works and how to implement a custom change_view (an edit form for any field(s) of your model)

The views generated in the admin are found in options.py in the django/contrib/admin/ directory. It seems that change_view is called to eventually get forms for each of the model’s fields. change view takes an object_id as a parameter. the view gets the model and object. The view also gets a form for the model:

ModelForm = self.get_form(request, obj)
...
form = ModelForm(instance=obj) 

Let’s analyze what self.get_form does:

    def get_form(self, request, obj=None, **kwargs):
        """
        Returns a Form class for use in the admin add view. This is used by
        add_view and change_view.
        """
        if self.declared_fieldsets:
            fields = flatten_fieldsets(self.declared_fieldsets)
        else:
            fields = None
        if self.exclude is None:
            exclude = []
        else:
            exclude = list(self.exclude)
        exclude.extend(kwargs.get("exclude", []))
        exclude.extend(self.get_readonly_fields(request, obj))
        # if exclude is an empty list we pass None to be consistant with the
        # default on modelform_factory
        exclude = exclude or None
        defaults = {
            "form": self.form,
            "fields": fields,
            "exclude": exclude,
            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
        }
        defaults.update(kwargs)
        return modelform_factory(self.model, **defaults)

First, what is self.declared_fieldsets? The relevant code is:

    def _declared_fieldsets(self):
        if self.fieldsets:
            return self.fieldsets
        elif self.fields:
            return [(None, {'fields': self.fields})]
        return None
    declared_fieldsets = property(_declared_fieldsets)

Basically, if you set the fieldsets parameter in the admin.py of your app, it will return with those fieldsets, or if you set the fields parameter, it will return those. In my case, I set the fields parameter in my admin.py and those are getting returned.

The next section of code deals with any excluded fields. Since I have none, I’m going to ignore this and go to defaults dictionary. In defaults, form corresponds to self.form, which I believe is the form set in BaseModelAdmin by form = forms.ModelForm .

Let’s see what happens with modelform_factory(self.model, **defaults). This comes from django.forms.models.py:

def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
                       formfield_callback=None):
    # Create the inner Meta class. FIXME: ideally, we should be able to
    # construct a ModelForm without creating and passing in a temporary
    # inner class.

    # Build up a list of attributes that the Meta object will have.
    attrs = {'model': model}
    if fields is not None:
        attrs['fields'] = fields
    if exclude is not None:
        attrs['exclude'] = exclude

    # If parent form class already has an inner Meta, the Meta we're
    # creating needs to inherit from the parent's inner meta.
    parent = (object,)
    if hasattr(form, 'Meta'):
        parent = (form.Meta, object)
    Meta = type('Meta', parent, attrs)

    # Give this new form class a reasonable name.
    class_name = model.__name__ + 'Form'

    # Class attributes for the new form class.
    form_class_attrs = {
        'Meta': Meta,
        'formfield_callback': formfield_callback
    }

    return ModelFormMetaclass(class_name, (form,), form_class_attrs)

Lots of weird stuff going on here. Somehow a form is being built. Unfortunately the form isn’t even really built here. ModelFormMetaclass is building it since what is returned is something returned by ModelFormMetaclass with arguments of class_name, (form,), form_class_attrs. ModelFormMetaclass is found in the same models.py. Let’s skip this for now…

Let’s go back to the change_view definition.

prefixes = {}
for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):
    prefix = FormSet.get_default_prefix()
    prefixes[prefix] = prefixes.get(prefix, 0) + 1
    if prefixes[prefix] != 1:
        prefix = "%s-%s" % (prefix, prefixes[prefix])
    formset = FormSet(instance=obj, prefix=prefix,
        queryset=inline.queryset(request))
    formsets.append(formset)

First, what does self.get_formsets and self.inline_instances do?

def get_formsets(self, request, obj=None):
    for inline in self.inline_instances:
        yield inline.get_formset(request, obj)

self.get_formsets looks at self.inline_instances which seems to be a list of inline objects which have the method get_formset.
Let’s look at what self.inline_instances actually is by looking at the __init__ definition.

self.inline_instances = []
for inline_class in self.inlines:
    inline_instance = inline_class(self.model, self.admin_site)
    self.inline_instances.append(inline_instance)

After looking through options.py, I couldn’t find self.inlines. I then realized that this is optionally set by the developer in the admin.py of the app. Since I’m not setting this parameter, I’ll assume there are no self.inlines and I’ll skip over this piece of code.

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

This part gets the adminForm which is the actual form I’ll be working with on the page that is rendered. Let’s see how this form is generated by looking into helpers.AdminForm found in django.contrib.admin.py.

class AdminForm(object):
    def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
        self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
        self.prepopulated_fields = [{
            'field': form[field_name],
            'dependencies': [form[f] for f in dependencies]
        } for field_name, dependencies in prepopulated_fields.items()]
        self.model_admin = model_admin
        if readonly_fields is None:
            readonly_fields = ()
        self.readonly_fields = readonly_fields

....

This is very abstract for me. To help me understand the AdminForm, I’m going to list out its properties:
self.form, self.fieldsets, self.prepopulated_fields, self.model_admin, self.readonly_fields.
In my case, I’m not setting any fieldset parameter in my admin.py so I’m not paying attention to that property of the AdminForm object. The same goes for prepopulated_fields. The important thing is just self.form, which is
just the form set by form = ModelForm(instance=obj) in options.py above.
Lets look at what ModelForm does and let’s also inspect the template that change_view uses to make sure that form is what we’re really interested in.

inline_admin_formsets = []

for inline, formset in zip(self.inline_instances, formsets):
    fieldsets = list(inline.get_fieldsets(request, obj))
    readonly = list(inline.get_readonly_fields(request, obj))
    inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
        fieldsets, readonly, model_admin=self)
    inline_admin_formsets.append(inline_admin_formset)
    media = media + inline_admin_formset.media

The code block above also shouldn’t be evaluated since there are no inlines (as explained above) which means there should be no self.inline_instances. That brings us to this last piece of code.

        context = {
            'title': _('Change %s') % force_unicode(opts.verbose_name),
            'adminform': adminForm,
            'object_id': object_id,
            'original': obj,
            'is_popup': "_popup" in request.REQUEST,
            'media': mark_safe(media),
            'inline_admin_formsets': inline_admin_formsets,
            'errors': helpers.AdminErrorList(form, formsets),
            'root_path': self.admin_site.root_path,
            'app_label': opts.app_label,
        }
        context.update(extra_context or {})
        return self.render_change_form(request, context, change=True, obj=obj)

What’s the point in all this? I’m trying to see if I can use Django’s built-in forms for an individual field of a given model. What if I pass in a single field so that only a form is built for that single field? I will try this.

(edit!) After doing some more research, I found out that you can do this by making a form based on ModelForm which allows you to specify the model and fields for your form.

from django.forms import ModelForm

class YourCustomModelForm(ModelForm):
    class Meta:
        model = YourModel
        fields = ('your_model_field',)

Your view for this form can look something like this:

def custom_form_view(request):
    if request.method == 'POST':
        obj = YourModel.objects.get(...)
        form=YourCustomModelForm(request.POST, instance=obj)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect('...')
        else:
            return HttpResponse('error, form is not valid...')
    obj = YourModel.objects.get(...)
    form = YourCustomModelForm(instance=obj)
    return direct_to_template(request, 'something.html',{'form':form})

After all that effort, I found an easy solution. If only I found it faster!

This entry was posted in Programming. Bookmark the permalink.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>