Django export PDF directly from view

I’m wondering if it’s somehow possible to process the PDF export directly in the main view, and have it triggered by a button click in the template. The reasoning is that in the main view (dispatcher_single) I have 300 lines of code of data manipulation (including charts) that I would like to export to PDF and would like to avoid having to copy/paste all this code from main view to the PDF export view.

Main view with a bunch of calculations:

views.py
def dispatcher_single(request, pk):
    dispatcher = Dispatcher.objects.select_related("company", "location").get(id=pk)

    # data manipulation here (about 300 lines of code)

    context = {
        'dispatcher': dispatcher
        'bunch_of_other_data': data
    }
    return render(request, 'dispatchers/dispatcher-single.html', context)

Another view for exporting to PDF:

views.py
from renderers import render_to_pdf

def dispatcher_statement_form(request, pk):
    dispatcher = Dispatcher.objects.get(id=pk)
    context = {
        'dispatcher': dispatcher,
    }
    response = renderers.render_to_pdf("pdf/statement_form.html", context)
    if response.status_code == 404:
        raise Http404("Statement form not found.")

    filename = "Statement_form.pdf"
    content = f"inline; filename={filename}"
    download = request.GET.get("download")
    if download:
        content = f"attachment; filename={filename}"
    response["Content-Disposition"] = content
    return response

And the render function:

renderers.py

def render_to_pdf(template_src, context_dict={}):
    template = get_template(template_src)
    html  = template.render(context_dict)
    result = BytesIO()
    pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
    if pdf.err:
        return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
    return HttpResponse(result.getvalue(), content_type='application/pdf')

I’m not sure I’m following what you’re asking for here. What is the sequence of events involved with this?

Are you asking if dispatcher_single can return a pdf? If so, the answer is yes, in exactly the same manner as you create and return a pdf in dispatcher_statement_form.

So I want the dispatcher_single to load as usual. In the template, I want to have a button that when clicked, to generate a PDF using data from the context of the main view (so that I don’t have to copy all the data processing code from the main view dispatcher_single to the generate PDF view dispatcher_statement_form. The question is how do I code the button/link so that it triggers the PDF generation.

I’m assuming relying on the browser’s own print to pdf functionality for the web page won’t work?

Directly as described, you don’t - at least not without a bit of work.

Always keep in mind that when a Django view returns a page, it’s done. It’s gone. All the instance variables in that view are no longer available.

Therefore, your button will not be accessing the “same” view. It is going to call a “new” view.
(Yes, it’s the same code, but a different instance.)

This means you need to work around this situation in one of a couple different ways.

  • As Tim points out above, use some type of browser-based functionality to print the page to PDF.

  • Use a JavaScript library to create the PDF in the browser.

  • Save the data that is used to generate the PDF and have your second view generate the PDF from that saved data.

    • You can save the data as a model in your database, in one of the caching packages, in the session, or even as a file.
    • You could also generate the PDF in the initial view, and save it in one of those locations.

If you’re only concerned about copying the code between the views and not about the time / processing requirements for generating the data, then the answer to that is to refactor those views. Extract that data processing code into a separate function to be called from either view.

1 Like

Just to note on relying on the browser: this is generally unreliable. I have a system where I was relying on (relatively) similar output from different browsers, but this seems not to be possible. Even where I am not changing the code. One user has been producing their own print/PDFs for months – now the layout is all over the place – I didn’t change anything, and they say they didn’t either (hmm…).

There’s a longstanding bug fix request for Safari (14 years I think) to behave like other browsers with regard to <thead>, but Apple seems not to be listening. There are probably many other issues between browsers too.

While (some) users can probably live without table headers on every page, others are getting the headers overlapping the content – which clearly is not tolerable.

So, since it is pretty much impossible to debug with individual users (“I did this but it did not work… but changing this might work…”) I’m going to switch the output to rendering PDFs directly – at least that way I know what I am (consistently) delivering.

1 Like

Thank you, Ken! This options seems like what I was looking for.

I asked ChatGPT and it gave me a solution: call the dispatcher_statement_form from the dispatcher_single after the context is defined:

if request.GET.get("export_pdf"):
        return dispatcher_statement_form(context, download=True)

and the PDF generate view:

def dispatcher_statement_form(context, download=False):
    context = context
    response = renderers.render_to_pdf("pdf/dispatcher_statement_form.html", context)
    if response.status_code == 404:
        raise Http404("Statement form not found.")

    filename = f"Statement_form_{dispatcher.first_name}_{dispatcher.last_name}.pdf"
    content = f"inline; filename={filename}"
    if download:
        content = f"attachment; filename={filename}"
    response["Content-Disposition"] = content
    return response

and in the template:

<a href="?export_pdf=1">Export</a>

This is doing exactly what I need.

That’s not really the “Djangoish” way of doing this, but yes, it will work.