Search code examples
djangodjango-admin-actions

Dynamic dropdown on intermediate page for admin action


I have a model Jar which has a crate attribute -- a ForeignKey to a Crate model. The Crate model has a capacity attribute (the number of jars it can hold) and a jars property (the number of jars it currently holds) which is this line: return self.jar_set.filter(is_active=True).count().

I have an admin action which moves multiple jars to a new crate. It uses an intermediate page to select the destination crate. Right now all crates are listed in the dropdown, but I want to limit the crates listed to only those with room for the number of jars selected. How?

Here's the admin action from admin.py:

class MoveMultipleJarsForm(forms.Form):
    # This needs to somehow be restricted to those crates that have room
    dest = forms.ModelChoiceField(queryset=Crate.objects.all().order_by('number'))

def move_multiple_jars(self, request, queryset):
    form = None

    if 'apply' in request.POST:
        form = self.MoveMultipleJarsForm(request.POST)

        if form.is_valid():
            dest = form.cleaned_data['dest']

            count = 0
            for jar in queryset:
                jar.crate = dest
                jar.save()
                count += 1

            plural = ''
            if count != 1:
                plural = 's'

            self.message_user(request, "Successfully moved %d jar%s to %s" % (count, plural, dest))
            return HttpResponseRedirect(request.get_full_path())
    if not form:
        form = self.MoveMultipleJarsForm()

    return render(request, 'admin/move_multiple_jars.djhtml', {
        'jars': queryset,
        'move_multiple_jars_form': form,
        })

move_multiple_jars.short_description = "Move multiple jars to new crate"

Solution

  • With the help of laidibug on the Python Developers Slack server, I was able to come up with a solution.

    class MoveMultipleJarsForm(forms.Form):
        dest = forms.ModelChoiceField(Crate.objects.none())
    
        def __init__(self, *args, **kwargs):
            count = kwargs.pop('count')
            super().__init__(*args, **kwargs)
            self.fields['dest'].queryset = Crate.objects.annotate(room=F('capacity')-Sum(Case(When(jar__is_active=True, then=1), default=0), output_field=IntegerField())).filter(room__gte=count).order_by('number')
    
    
    def move_multiple_jars(self, request, queryset):
        form = None
    
        if 'apply' in request.POST:
            form = self.MoveMultipleJarsForm(request.POST)
    
            if form.is_valid():
                dest = form.cleaned_data['dest']
    
                count = 0
                for jar in queryset:
                    jar.crate = dest
                    jar.save()
                    count += 1
    
                plural = ''
                if count != 1:
                    plural = 's'
    
                self.message_user(request, "Successfully moved %d jar%s to %s" % (count, plural, dest))
                return HttpResponseRedirect(request.get_full_path())
        if not form:
            form = self.MoveMultipleJarsForm(count=queryset.count())
    
        return render(request, 'admin/move_multiple_jars.djhtml', {
            'jars': queryset,
            'move_multiple_jars_form': form,
            })
    
    move_multiple_jars.short_description = "Move multiple jars to new crate"
    

    The first step toward the solution was to modify the form class to pass the number of jars in the queryset as an initialization value which could then change the list of destination crates. I had a 90% solution with Subquery and OuterRef but it couldn't handle crates with no active jars. I went with the accepted answer in this question: How to filter objects for count annotation in Django? Oddly enough, my 90% solution is similar to their solution for Django 1.11 but I guess they didn't worry about the no-participants case. I gave the accepted answer in that question another up-vote because if I had seen it originally I might not have had to post this question.