Search code examples
pythondjangoadministrationmanytomanyfield

Django Administration - Customize list appearance and selection of ManyToManyField keys


I have a database with 1000+ songs. I have a custom model "Schedule" that accepts songs as field.

models.py

from django.db import models

class Song(models.Model):
    title = models.CharField(max_length=255)
    words = models.TextField()
    slug = models.SlugField()
    date = models.DateTimeField(auto_now_add=True)
    snippet = models.CharField(max_length=50)

    def __str__(self):
        return self.title

class Schedule(models.Model):
    songs = models.ManyToManyField(Song)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return str(self.date)

admin.py

from django.contrib import admin
from .models import Song, Schedule

@admin.register(Song)
class SongModel(admin.ModelAdmin):
    search_fields = ('title',)
    list_display = ('title',)
    list_per_page = 100

@admin.register(Schedule)
class ScheduleModel(admin.ModelAdmin):
    search_fields = ('date',)
    list_display = ('date',)
    list_per_page = 100

I want to be able to add any song I want to a schedule, but it is difficult do to so through the default list in the Django-Administration, which looks like this. I have to scroll and CTRL+select each one of them, then add them.

I'd like something more more practical where I can select, search, etc.

What are my options? I don't know where to start looking.


Solution

  • Option 1

    It's only comfortable if you have very few related items (few songs in schedule). But it is super easy and will be better than what you have now. (django.contrib.admin comes with built-in select2.)

    @admin.register(Schedule)
    class ScheduleAdmin(admin.ModelAdmin):
        ...
        autocomplete_fields = ("songs",)
    
    

    Option 2

    (upd: damn, forgot it at first, it's also super simple and quite efficient)

    It looks alright-ish. Usable. Not particularly comfortable. But better than ctrl-clicking the stuff.

    @admin.register(Schedule)
    class ScheduleAdmin(admin.ModelAdmin):
        ...
        filter_horizontal = ('songs',)
    

    Option 3

    If you want a comfortable UI without implementing custom pages or actions (which are unfortunately a complete mess), you should use a StackedInline admin.

    It's quite a bit more difficult though.

    First you will need a through model. (I don't think inlines are possible with auto-generated many-to-many models.) It's basucally your many-to-many between the two models. Something like that:

    class ScheduleSongNM(models.Model):
        song = models.ForeignKey("Song", null=False)
        schedule = models.ForeignKey("Schedule", null=False)
    

    Tell your Schedule model to use your custom through model:

    class Schedule(models.Model):
        songs = models.ManyToManyField(Song, through="ScheduleSongNM")
    

    Now create an inline admin for the ScheduleSongNM:

    class ScheduleSongInline(admin.StackedInline):
        model = ScheduleSongNM
        fields = ["song"]
        autocomplete_fields = ["song"]  # select2 works here too
    

    Finally, tell your Schedule admin that it has an inline now:

    @admin.register(Schedule)
    class ScheduleAdmin(admin.ModelAdmin):
        ...
        inlines = [ScheduleSongInline]
        ...
    

    Maybe I missed something (obviously I haven't tested it), but I think you got the general idea. In the end you get a box inside of your Schedule admin that looks something like that (plus auto completion for song names):

    screenshot