Generate multiple PDFs and zip them for download, all in a single view
Asked Answered
O

2

8

I am using xhtml2pdf to generate PDFs in my Django View. The idea is to loop over all the instances that are there in the query, then for each instance create a PDF, then add all the generated PDFs to one zip File for download. The xtml2pdf logic is working okay but the looping logic is what gives me headache.

So this is my function so far:

def bulk_cover_letter(request, ward_id, school_cat_id, cheque_number):
    school_type = SchoolType.objects.get(id=school_cat_id)

    schools_in_school_type = Applicant.objects.filter(
        school_type=school_type, ward_id=ward_id, award_status='awarded'
    ).order_by().values_list('school_name', flat=True).distinct()

    for school in schools_in_school_type:
        beneficiaries = Applicant.objects.filter(school_type=school_type, ward_id=ward_id, award_status='awarded', school_name=school)
        total_amount_to_beneficiaries = Applicant.objects.filter(school_type=school_type, ward_id=ward_id, award_status='awarded', school_name=school).aggregate(total=Sum('school_type__amount_allocated'))
        context = {
            'school_name' : school,
            'beneficiaries' : beneficiaries,
            'total_amount_to_beneficiaries' : total_amount_to_beneficiaries,
            'title' : school + ' Disbursement Details',
            'cheque_number': cheque_number
        }

        response = HttpResponse('<title>Cover Letter</title>', content_type='application/pdf')
        filename = "%s.pdf" %(cheque_number)
        content = "inline; filename=%s" %(filename)
        response['Content-Disposition'] = content
        template = get_template('cover_letter.html')
        html = template.render(context)
        result = io.BytesIO()
        pdf = pisa.CreatePDF(
            html, dest=response, link_callback=link_callback)
        if not pdf.error:
            # At this point I can generate a single PDF.
            # But no idea on what to do next.

    # The zipping logic should follow here after looping all the instances - (schools)

From that Point I have no idea on what to do next. Any help will be highly appreciated.

Overbalance answered 11/1, 2020 at 15:37 Comment(0)
U
3

Try this:

Utils.py

def render_to_pdf(template_src, context_dict={}):
    template = get_template(template_src)
    html  = template.render(context_dict)
    buffer = BytesIO()
    p = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), buffer)
    pdf = buffer.getvalue()
    buffer.close()
    if not p.err:
        return pdf#HttpResponse(result.getvalue(), content_type='application/pdf')
    return None


def generate_zip(files):
    mem_zip = BytesIO()

    with zipfile.ZipFile(mem_zip, mode="w",compression=zipfile.ZIP_DEFLATED) as zf:
        for f in files:
            zf.writestr(f[0], f[1])

    return mem_zip.getvalue()

Views.py

def generate_attendance_pdf(modeladmin, request, queryset):

    template_path = 'student/pdf_template.html'

    files = []

    for q in queryset:
        context = {
            'firstname': q.firstname,
            'lastname': q.lastname,
            'p_firstname': q.bceID.firstname
        }
        pdf = render_to_pdf(template_path, context)
        files.append((q.firstname + ".pdf", pdf))

    full_zip_in_memory = generate_zip(files)

    response = HttpResponse(full_zip_in_memory, content_type='application/force-download')
    response['Content-Disposition'] = 'attachment; filename="{}"'.format('attendnace.zip')

    return response

Obviously, you have to modify the context/names to what you need.

Credit to -> Neil Grogan https://www.neilgrogan.com/py-bin-zip/

Understrapper answered 30/5, 2020 at 1:50 Comment(0)
B
0

If you need to generate several PDF files and send them as a response in a zip file then you can store the reports in memory and set it as dest when you call pisa.CreatePDF. Then have a list of reports in memory, zip them, and send as a Django response specifying another content type.

For example:

reports = tempfile.TemporaryDirectory()
report_files = {}
for school in schools_in_school_type:
    # ... same code that renerates `html`
    mem_fp = BytesIO()
    pisa.CreatePDF(html, dest=mem_fp)
    report_files[filename] = mem_fp
mem_zip = BytesIO()
with zipfile.ZipFile(mem_zip, mode="w") as zf:
    for filename, content in report_files.items():
            zf.write(filename, content)
response = HttpResponse(mem_zip, content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="{}"'.format('cover_letters.zip')

This still generates an error of [Errno 2] No such file or directory: 'cheque_number.pdf'.

Bohr answered 11/1, 2020 at 15:57 Comment(6)
You should be able to create a zipfile.ZipFile from a BytesIO object that you can write the PDFs to directly I think without the need for a tempfileEnenstein
@IainShelvington could you please share your function?Overbalance
@Yann, could you be more elaborate on this part? you can create temporary files using tempfile and set it as dest when you call pisa.CreatePDF. Then have a list of these temp file paths . Probably use some code?Overbalance
@Bohr I'm getting this error: stat: path should be string, bytes, os.PathLike or integer, not _io.BytesIOOverbalance
I edited the zf.write(content, filename) to zf.write(filename, content) but now I get the error - File not found, <cheque_number>.pdf. Any insights?Overbalance
Did anyone get the answer to this?Hinny

© 2022 - 2024 — McMap. All rights reserved.