download button next to upload file in admin

I want to be able to download something I uploaded in the admin right next to the upload field.

Right now I upload a pdf using this method:

class OrderAdminForm(forms.ModelForm):
    invoice_file = forms.FileField(required = False, label = "invoice PDF")
    
    class Meta:
        model = Order
        fields = ["product", "manufacturer", "order_date",]
        
    def clean_invoice_file(self):
        cleaned_data = super().clean()

        if "invoice_file" in self.changed_data:
            print("invoice_file file was uploaded")
            cleaned_data["invoice"] = cleaned_data["invoice_file"].read()
        return cleaned_data
    
    def clean(self):
        cleaned_data = super().clean()
        if "invoice_file" in self.changed_data:
            self.instance.invoice = self.cleaned_data["invoice"]
        return cleaned_data
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["product", "manufacturer", "order_date",]
    list_filter = ["product", "manufacturer",]
    form = OrderAdminForm
    change_form_template = "download_invoice_button.html"

    def save_model(self, request, obj, form, change):
        obj.user = request.user
        return super().save_model(request, obj, form, change)
    
    def file_size(self, obj):
        return f"""{len(obj.file)/1024:,.2f} KByte"""
    
    def response_change(self, request, obj):
        if "_download_invoice" in request.POST:
            response = FileResponse(obj.get_file)
            response['Content-Type'] = 'application/pdf'
            response["Content-Disposition"] = f"""attachment; filename="{obj.get_file_name}" """
            return response
        else:
            return super().response_change(request, obj)

and I added a download button in the download_invoice_button.html like this:

{% extends 'admin/change_form.html' %}
{% block submit_buttons_bottom %}
{{ block.super }}
<div class="submit-row">
    <input type="submit" value="Download Invoce" name="_download_invoice">
</div>
{% endblock %}

But now my change_form looks like this:

image

And I would like to achieve this look:

image

How can this be done?

For completness - The model:

class Order(Comment):
    product         = ForeignKey(Product, on_delete=CASCADE)
    manufacturer    = CharField(max_length=128)
    order_date      = DateField(default = timezone.now, editable=True)
    invoice         = BinaryField(editable=True, blank=True)

    @property
    def get_file_name(self):
        return f"""{self.manufacturer}_{self.order_date}.pdf"""
    
    @property
    def get_file(self):
        return io.BytesIO(self.invoice)
1 Like

Right off-hand, I’m going to guess that you’d need to create a new widget to display that button along with the “Choose…” button. You’ll then need to create a custom admin form to have that field use your new widget.

As always, thanks for the hint & insight. Would you be so kind to comment on my solution?

adding the form_instance to the widget and modifying the invoice_file custom field:

class OrderAdminForm(forms.ModelForm):
    invoice_file = forms.FileField(widget=CustomFileField, required = False, label = "invoice PDF")
   
     def __init__(self, *args, **kwargs): 
        super().__init__(*args, **kwargs)
        self.fields["invoice_file"].widget.form_instance = self 
   
     class Meta:
        model = Order
        fields = ["product", "manufacturer", "invoice_file", "order_date",]

The widget:

from django.contrib.admin.widgets import AdminFileWidget
class CustomFileField(AdminFileWidget):
    def render(self, name, value, attrs=None, renderer=None):
        output = [super().render(name, value)]
        try:
            if self.form_instance.instance.file == b"":
                raise Exception
            output.append("""<input type="submit" value="Download" name="_download_invoice">""")
        except:
            pass
        return "".join([format_html(x) for x in output])