11

Assuming my model looks like this (this is a simplified example):

class Person(Model):
  first_name = CharField(...)
  last_name = CharField(...)

  def name():
    return first_name + ' ' + last_name

Displaying the name as a single column in the admin change list is easy enough. However, I need a single, editable "name" field that is editable from the list page, which I can then parse to extract and set the model field values. The parsing isn't a concern. I am just wondering how to have an editable form field on the list page that doesn't correspond directly to a model field.

B Robster
  • 40,605
  • 21
  • 89
  • 122
  • You should consider moving away from separating `first_name` and `last_name`. Django avoids the topic because so much of the codebase is based on this idea. Check out this for more information: https://www.w3.org/International/questions/qa-personal-names – Bobort Jul 21 '22 at 16:29

3 Answers3

37

You should be able to do this in pure Python with a bit of work. Basically, you need to use the get_changelist_form method on the admin class to tell it to use a custom form rather than a default ModelForm for your instances, then initialize the custom field's value properly (most conveniently in the form's __init__ method) and specialize the save behavior of that form to set the first_name and last_name values.

Something like this should be a start:

class PersonChangeListForm(forms.ModelForm):
    class Meta:
        model = Person
    name = forms.CharField()

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance')
        if instance:
            initial = kwargs.get('initial', {})
            initial['name'] = '%s %s' % (instance.first_name, instance.last_name)
            kwargs['initial'] = initial
        super(PersonChangeListForm, self).__init__(*args, **kwargs)

    def save(self, *args, **kwargs):
        # use whatever parsing you like here
        first_name, last_name = self.cleaned_data['name'].split(None, 1)
        self.cleaned_data['first_name'] = first_name
        self.cleaned_data['last_name'] = last_name
        return super(PersonChangeListForm, self).save(*args, **kwargs)

class PersonAdmin(admin.ModelAdmin):
    def get_changelist_form(self, request, **kwargs):
        return PersonChangeListForm

You will also need to declare a list_editable value that evaluates to True when tested as a boolean - some of the admin processing short-circuits without using the formset if list_editable does not evaluate as True.

If you have no other fields you want to be editable, this gets more complicated. The class validation requires that everything in the list_editable sequence be an editable field that's declared in list_display as well and is not a display link field. I think the options there are either to override the admin class's changelist_view method to use the full processing even if list_editable is not true, or to define a custom subclass of list or tuple that evaluates to True even when empty so it can pass validation. The former would require repeating a great deal of standard code and significantly increase your maintenance burden if you ever upgrade, while the latter is a counterintuitive hack and would not at all surprise me if it had unexpected consequences.

Neither are good options, so I hope you have at least one other field that makes sense to include in list_editable.

DrumRobot
  • 393
  • 1
  • 3
  • 15
Peter DeGlopper
  • 36,326
  • 7
  • 90
  • 83
  • Thanks Peter, looks promising. I'll check it out. – B Robster Oct 12 '13 at 18:10
  • 2
    This definitely put me on the right track... I had to use one of the actual model field columns (i.e., 'first_name') in the list_editable field, just changed the display name to "full name" and then used the __init__ and save method to actually parse out the values and save them. As far as I could figure out, I wasn't able to get a non-model form field to show up as a column in the list page. But it worked out. Thanks! – B Robster Oct 21 '13 at 16:51
  • Hey, hope you see my comment. I used your approach but I can't manage `list_editable` to work, Django demands that everything on that list must be field of model, so I couldn't convince it to use field of Form instead, so this approach failed for me. Any ideas what am I doing wrong? – Andrey Jun 17 '15 at 16:35
  • @Andrey - I haven't looked at it in a while, but if I recall correctly the custom form field (`name` in the above example) does not need to be in `list_editable`. You just need to have something there so Django creates the formset. The last two paragraphs of my answer are my best take on what your options are if you have no model fields that you want to be editable, just the custom field. – Peter DeGlopper Jun 17 '15 at 17:30
  • @Ben Roberts, I follow your method, it can show editable field "name" in change list, then I change the "name" field value and click "save" button, Django didn't save my edit to "name" field. I run Debug, and I found that the function save(self, *args, **kwargs) in class PersonChangeListForm(forms.ModelForm) is not be called. – GoTop Apr 16 '16 at 10:19
2

I just tried a quick mock-up of the problem in the admin. It seems that the admin validation fails for a field which is in list_editable that is not defined on the model. In short, the answer to your question seems to be no.

However, that doesn't mean it's not doable. With a bit of Javascript, you could use X-editable

(or roll your own), and make the "Name" column editable. Create a view to validate the data and save it to the model. Set X-editable field 'url' parameter to post to this URL. Obviously decorate your view with login_required / permissions_required etc, to make sure no-one else can edit the data.

cazgp
  • 1,538
  • 1
  • 12
  • 26
1

Addition to @Peter DeGlopper answer: you may add other model field in list_editable (for example last_name), then override change_list.html (look this answer How to override templates and add to the end of file some javascrip code: templates/admin/your_app/person/change_list.html:

{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_list %}
...

{% block content %}
...
{% endblock %}

{% block footer %}
{{ block.super }}
<script>
    var last_names = document.querySelectorAll('.field-last_name');

    last_names.forEach(function(el) {
        var input = el.children[0];
        var text = input.value;
        input.hidden = true;
        input.style.display = 'none';
        el.append(text);
    });
</script>
{% endblock %}
Community
  • 1
  • 1
Nikita Isaev
  • 106
  • 2