ARG-JR / ResumeBuilder

0 stars 0 forks source link

Figure out how to export pdf files #2

Open Alex-Garza opened 2 years ago

Alex-Garza commented 2 years ago

Research needs to be done to determine how we should export a resume as a pdf file using a django template.

Ideally we should be able to allow the user to select one of many different templates that we create to export their resume.

jtrenaud1s commented 2 years ago

Article: https://codeburst.io/django-render-html-to-pdf-41a2b9c41d16 (It's a medium article so after clicking it so many times you'll lose access to it, so I'm posting the code too.

Article Contents

Lets setup our initial application. Create a new Django application, setup virtualenv and create a couple of models and register them in our Admin.

### admin.py

from django.contrib import admin
from .models import *

admin.site.register(Sales)
admin.site.register(Products)
### models.py

from django.db import models
from django.contrib.auth.models import User

class Products(models.Model):
    title = models.CharField(max_length=255)
    price = models.DecimalField(default=0.00, max_digits=18, decimal_places=2)

    def __str__(self):
        return self.title

    class Meta:
        db_table = "tutorial_products"
        verbose_name = "Product"
        verbose_name_plural = "Products"

class Sales(models.Model):
    product = models.ForeignKey(Products, on_delete=None)
    quantity = models.IntegerField(default=0)
    price = models.DecimalField(default=0.00, max_digits=18, decimal_places=2)
    customer = models.ForeignKey(User, on_delete=None)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.product

    def save(self, *args, **kwargs):
        self.price = self.product.price * self.quantity
        super(Sales, self).save(*args, **kwargs)

    class Meta:
        db_table = "tutorial_product_sales"
        verbose_name = "Sale"
        verbose_name_plural = "Sales"

The only not ordinary aspect her is that we override the save method of the sales model to calculate the price. Lets make and migrate these.

$ python manage.py makemigrations
$ python manage.py migrate

Our app is in its basic form, lets create some users of our app, we need a superuser, and a 2 more users for this example (be sure to give the first and last names)

We will be building a sales report, so lets populate our database. We dont want to manually go through the admin panel and add these records, so lets build a simple seeder, a method we can run which will populate our database for us.

### seed.py

from .models import *
from random import *
from decimal import Decimal

class Seeder:

    def __init__(self):
        self.products = ["Orange Ball", "Chew Toy 1", "Cat Bowl", "Dog Bed", "Cat Food", "Dog Food"]

    def seed(self):
        for x in range(20):
            title = choice(self.products) + " {0}".format(randint(1, 10000))
            price = float(format(Decimal(str(random())), '.2f'))
            quantity = randint(1, 100)
            customer = User.objects.get(pk=randint(1,3))
            product = Products(title=title, price=price)
            product.save()
            sale = Sales(product=product, quantity=quantity, customer=customer)
            sale.save()

This class just creates a list of product titles when instantiated and then in the method seed it will run 20 times and create us 20 products and 20 sales, we use random to generate some random attributes for us.

We call this by running it from our terminal, we could build a management command to run this if needed, for a walkthrough of management commands see here.

Our database has been populated now, so lets build our report, the first thing we need is to bring in the package we will use.

$ pip install --pre xhtml2pdf

Lets setup our render class, we will be building a wrapper class that holds our logic for building our pdf. This file is render.py

### render.py

from io import BytesIO
from django.http import HttpResponse
from django.template.loader import get_template
import xhtml2pdf.pisa as pisa

class Render:

    @staticmethod
    def render(path: str, params: dict):
        template = get_template(path)
        html = template.render(params)
        response = BytesIO()
        pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), response)
        if not pdf.err:
            return HttpResponse(response.getvalue(), content_type='application/pdf')
        else:
            return HttpResponse("Error Rendering PDF", status=400)

We create a class called Render and build out a static method called render, this takes a string and dict as arguments. We then use some django methods to get the template and render it using our dict argument.

The pisa.pisaDocument method accepts streams as parameters we need a source and destination. we encode our template to UTF-8 and parse that into a BytesIO stream and then use the reponse object we created to hold the output from pisa.

We then check for errors during the processing and return HttpResponse object, for success we specify we want the response.getValue() and set the content_type meta tag as ‘application/pdf’.

Lets build out our views.py

### views.py

from django.views.generic import View
from django.utils import timezone
from .models import *
from .render import Render

class Pdf(View):

    def get(self, request):
        sales = Sales.objects.all()
        today = timezone.now()
        params = {
            'today': today,
            'sales': sales,
            'request': request
        }
        return Render.render('pdf.html', params)

We implement a standard CBV, with just a get method, we build our params dictionary and return a Render.render() object. We don’t need to return a Httpresponse as this is handled by the Render.render method.

So now we need our template:

### pdf.html

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Sales Report</title>
    <style type="text/css">
        @page {
            size: A4;
            margin: 1cm;
        }

        .table {
            width: 100%;
            max-width: 100%;
            margin-bottom: 5px;
            background-color: #fff;
        }

        .table th,
        .table td {
            padding: 5px;
            vertical-align: top;
            border-top: 1px solid #000;
            text-align: center;
        }

        .table thead th {
            vertical-align: bottom;
            border-bottom: 2px solid #000;
        }

        .table tbody + tbody {
            border-top: 2px solid #000;
        }

        .table .table {
            background-color: #fff;
        }

        .list-group {
            display: block;
            width: 100%;
            list-style: none;
            margin-top: 15px;
            margin-bottom: 15px;
        }

        .list-group p {
            width: 100%;
            height: 20px;
            line-height: 20px;
            list-style: none;
            font-size: 1.1em;
        }

    </style>
</head>
<body>

<div class="container">
    <div class="card">
        <div class="card-header">
            <h3>Sales Report - {{ today | date:"d/m/Y" }}</h3>
        </div>

        <div class="list-group">
            <p>Name: {{ request.user.first_name }} {{ request.user.last_name }}</p>
        </div>

        <table class="table">
            <thead>
            <tr>
                <th>Raised</th>
                <th>Product</th>
                <th>Quantity</th>
                <th>Price</th>
                <th>Customer</th>
            </tr>
            </thead>
            <tbody>
            {% for sale in sales %}
                <tr>
                    <td>{{ sale.created_at | date:"d/m/Y" }}</td>
                    <td>{{ sale.product.title }}</td>
                    <td>{{ sale.quantity }}</td>
                    <td>&pound;{{ sale.price }}</td>
                    <td>{{ sale.customer.first_name }} {{ sale.customer.last_name }}</td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
</div>

</body>
</html>

You may run into trouble pulling external stylesheets, ideally i would suggest you keep your styling inline.

Lets bind our view to a URL, in a real project i would create a urls.py as normal, however for this example i have just bound it to the main urls file.

path('render/pdf/', Pdf.as_view())

Lets give it a test, inside Chrome i get the following:

image