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"
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.