Skip to content

Instantly share code, notes, and snippets.

@jdklub
Last active November 22, 2023 11:25
Show Gist options
  • Save jdklub/9261959 to your computer and use it in GitHub Desktop.
Save jdklub/9261959 to your computer and use it in GitHub Desktop.
Edit a many-to-many relationship (ManyToManyField) on the Django Admin change list page.
class BookChangeList(ChangeList):
def __init__(self, request, model, list_display, list_display_links,
list_filter, date_hierarchy, search_fields, list_select_related,
list_per_page, list_max_show_all, list_editable, model_admin):
super(BookChangeList, self).__init__(request, model, list_display, list_display_links,
list_filter, date_hierarchy, search_fields, list_select_related,
list_per_page, list_max_show_all, list_editable, model_admin)
# these need to be defined here, and not in BookAdmin
self.list_display = ('name', 'categories')
self.list_display_links = ['name']
self.list_editable = ['categories']
class BookForm(forms.ModelForm):
# this the bit of custom CSS we want to add
style_text = "height:80px; overflow-y:scroll;"
# here we only need to define the field we want to be editable
categories = forms.ModelMultipleChoiceField(queryset=BookCategory.objects.all(),widget=StyleableCheckboxSelectMultiple(ul_attrs={"style":style_text}),required=False)
class BookAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
return BookChangeList
def get_changelist_form(self, request, **kwargs):
return BookForm
admin.site.register(Book, BookAdmin)
# this subclass allows us to add a bit of our own CSS at the <ul> level
class StyleableCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
def __init__(self, attrs=None, ul_attrs=None):
self.ul_attrs = ul_attrs
super(ScrollableCheckboxSelectMultiple, self).__init__(attrs)
def render(self, name, value, attrs=None, choices=()):
html = super(ScrollableCheckboxSelectMultiple, self).render(name, value, attrs, choices)
final_attrs = self.build_attrs(self.ul_attrs)
return mark_safe(html.replace('<ul>','<ul%s>' % flatatt(final_attrs)))
class BookCategory(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
name = models.CharField(max_length=255)
categories = models.ManyToManyField('BookCategory', blank=True, null=True)
def __unicode__(self):
return self.name
@mick-t
Copy link

mick-t commented Nov 20, 2014

Very helpful, thank you!

@mick-t
Copy link

mick-t commented Dec 2, 2014

When I implemented this I noticed that the checkbox on the left side of the admin were missing, to have them display I had to add action_checkbox to the list_display in the ChangeList class.

Like this:

class BookChangeList(ChangeList):
    def __init__(self, request, model, list_display, list_display_links,
            list_filter, date_hierarchy, search_fields, list_select_related,
            list_per_page, list_max_show_all, list_editable, model_admin):

        super(BookChangeList, self).__init__(request, model, list_display, list_display_links,
            list_filter, date_hierarchy, search_fields, list_select_related,
            list_per_page, list_max_show_all, list_editable, model_admin)

        # these need to be defined here, and not in BookAdmin
        self.list_display = ('action_checkbox', 'name', 'categories')
        self.list_display_links = ['name']
        self.list_editable = ['categories']

@efimerdlerkravitz
Copy link

Made couple pf fixes to support Python3 and Django 1.11. See https://gist.github.com/efimerdlerkravitz/6177b656563e86f57f1fb81c1a18bf85

@quxf2012
Copy link

quxf2012 commented Nov 22, 2023

"""
base https://gist.github.com/jdklub/9261959

https://gist.github.com/quxf2012/b1827d2bf02322c2779e8309ba089163

usage:
@admin.register(XxxModel)
class XxxModelAdmin(ModelMultipleListEdit, admin.ModelAdmin):
    m_list_editable = {'mtomfield': MtomModel.objects.all()}  # ModelMultipleListEdit use

"""
from django import forms
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe

__all__ = ['ModelMultipleListEdit']


class StyleableCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
    def __init__(self, attrs=None, ul_attrs=None):
        self.ul_attrs = ul_attrs
        super().__init__(attrs)

    def render(self, *args, **kwargs):
        html = super().render(*args, **kwargs)
        final_attrs = self.build_attrs(self.ul_attrs)
        return mark_safe(html.replace('<ul>', '<ul%s>' % flatatt(final_attrs)))


class ModelMultipleListEdit:
    m_list_editable = None  # dict # m_list_editable={ "field": field_queryset }      #field_queryset

    def get_changelist_form(self, request, **kwargs):
        if not self.m_list_editable:
            return super().get_changelist_form(request, **kwargs)

        # dynamic create Form
        cls_dict = {}
        for field, queryset in self.m_list_editable.items():
            # dynamic create ManyToManyField form field
            cls_dict[field] = forms.ModelMultipleChoiceField(queryset=queryset, widget=StyleableCheckboxSelectMultiple(ul_attrs={"style": "height:80px; overflow-y:scroll;"}), required=False)

        cls = type('CloudAppForm', (forms.ModelForm,), cls_dict)
        return cls

    def get_changelist_instance(self, request):
        changeList = super().get_changelist_instance(request)
        if not self.m_list_editable:
            return changeList
        # injection ManyToManyField to list_display list_editable ;
        # use to  skip  ManyToManyField check;
        list_display = list(self.list_display)
        list_editable = list(self.list_editable)
        for i in self.m_list_editable:  # key
            if i not in list_display:
                list_display.append(i)
            if i not in list_editable:
                list_editable.append(i)

        self.list_display = list_display
        self.list_editable = tuple(list_editable)

        # fix first request display
        changeList.list_display = list_display
        if changeList.is_popup:
            changeList.list_editable = ()
        else:
            changeList.list_editable = list_editable
        # fix first request display

        return changeList

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment