Create an issuer¶
Once Marion is installed, you will need to create one issuer per document type you need to generate. Your issuers should stand in a python module that bundles templates and the business logic required to build your documents. This module can be distributed as a python package or a core Django application in your Django project.
Module tree¶
An example shop
module tree follows:
apps/shop
├── defaults.py
├── __init__.py
├── issuers
│ ├── __init__.py
│ └── invoice.py
├── static
│ └── shop
│ └── logo.svg
├── templates
│ └── shop
│ ├── invoice.css
│ └── invoice.html
└── tests
└── issuers
├── __init__.py
└── test_invoice.py
As you may have noticed if you are familiar with Django: the module tree looks
like a standard Django app (but without models, views and urls). Except for
the static/{{ application_name }}
directory, the project tree is only a
recommendation, feel free to organize things the way you like.
In the proposed project tree:
defaults.py
contains default values to configure your issuer,issuers
module contains issuers (one file per issuer),static/{{ application_name }}
is the place to store images that will be embedded in your rendered documents,templates/{{ application_name }}
contains your HTML and CSS templates that will be used to generate your documents, and,tests/issuers
directory will ship tests for your issuers (business logic).
Business logic¶
Now that your project tree is ready, you will need to write code for your
Invoice
issuer. Before writing code, we will have to explain a key concept in
Marion’s design: an issuer uses a context query to fetch a context that
will be used to substitute variables in your templates. In other words, this
context query is a collection of key-values that is required to build or fetch
a collection of key-values that will serve as the context.
An example issuer code follows (it will be commented in details later):
# apps/shop/issuers/invoice.py
import json
from pathlib import Path
from uuid import UUID
import requests
from pydantic import BaseModel
from marion.issuers.base import AbstractDocument
class Customer(BaseModel):
"""Customer pydantic model"""
name: str
class Invoice(BaseModel):
"""Invoice pydantic model"""
invoice_id: UUID
customer: Customer
total: float
class ContextModel(BaseModel):
"""Context pydantic model"""
invoice: Invoice
class ContextQueryModel(BaseModel):
"""Context query pydantic model"""
order_id: UUID
class InvoiceDocument(AbstractDocument):
"""Invoice issuer"""
keywords = ["MyShop", "invoice"]
context_model = ContextModel
context_query_model = ContextQueryModel
css_template_path = Path("shop/invoice.css")
html_template_path = Path("shop/invoice.html")
def fetch_context(self) -> dict:
"""Write your business logic to fetch the context here"""
response = requests.get(
f"https://www.myshop.com/api/orders/{self.context_query.order_id}"
)
order = json.loads(response.json())
return {
"invoice": {
"invoice_id": order.get("id"),
"customer": {
"name": order.get("customer").get("fullname"),
},
"total": order.get("total"),
}
}
def get_title(self):
"""Generate a PDF title that depends on the context"""
return f"Invoice ref. {self.context.invoice.invoice_id}"
After reading this simplified piece of code, you may have noticed that:
- your issuer class should inherit from the
marion.issuers.base.AbstractDocument
class, - your issuer class should implement the
fetch_context
method, - the
fetch_context
method should return a dictionnary of the context that will be used to render your templates (more on this later), - you should define Pydantic models to validate data from your context and context query,
Note that documents metadata such as the title
, keywords
or authors
can
be statically set as an issuer class attribute (e.g. title
) or dynamically
using the corresponding method (e.g. get_title()
for the title
attribute
in our example). For reference, see the
marion.issuers.base.PDFFileMetadataMixin
mixin
implementation.
Document templates¶
While writing our issuer class, we’ve taken care of the business logic to collect all required information (context variables) that will be integrated to the issuer document template. The second step is to implement the logical structure (HTML) and the design (CSS) of our document.
While writing your document template, you must keep in mind that you are
designing a printed document, e.g. writing CSS rules for the print
media.
You should also note that both your HTML and CSS files are Django templates that are consequently context-aware and versatile.
Simplified example template files for the Invoice
issuer are presented below.
<!-- apps/shop/templates/shop/invoice.html -->
{% load i18n %}
{% load static %}
<html>
{% if debug %}
<head>
<style>
{{ css }}
</style>
</head>
{% endif %}
<body>
<div class="invoice">
<header>
<!--
Company matters
-->
<div class="logo">
<img
src="{{ debug | yesno:",file://" }}{% static "shop/logo.svg" %}"
alt="{% trans "company logo" %}"
/>
</div>
</header>
<article class="order">
<div class="invoice-id">
{% trans "Invoice reference:" %} {{ invoice.invoice_id }}
</div>
<div class="customer">
{{ invoice.customer.name }}
</div>
<div class="total-amount">
{{ invoice.total }} €
</div>
</article>
<footer>
<!--
Contact informations
-->
</footer>
</div>
</body>
</html>
If you are familiar with Django templates,
debug
blocks usage or conditions can be confusing at first sight. We will explain those in the next subsection.
/* apps/shop/templates/shop/invoice.css */
/* load extra fonts */
@import url("https://fonts.googleapis.com/css2?family=Open+Sans");
body {
font-family: "Open Sans", sans-serif;
font-size: 11pt;
color: #222;
}
@media print {
/* ----------------------
* Reset margins for media
* ---------------------- */
@page {
size: A4 portrait;
margin: 0;
padding: 0;
}
body {
padding: 0;
background: #ffbe0b;
}
* {
margin: 0;
padding: 0;
}
/* ----------------------
* Add custom styles below
* ---------------------- */
.invoice {
/* [...] */
}
}
Using the document template debug view¶
Integrating a document template can be time consuming if you need to render it as a PDF every time you want to check how it looks like. To ease your life, we’ve cooked a template debug view that can be activated in your development environment by modifiying your root URLs configuration as follow:
# myproject.myproject.urls
from django.conf import settings
# [...]
if settings.DEBUG:
urlpatterns += [path("__debug__/", include("marion.urls.debug"))]
We advice you not to activate this in production, it should only be active for development.
By using this view, you will be able to “see” your document in your browser as a normal web page at the following URL: http://localhost:8000/__debug__/templates/
Two GET request parameters are required to point to your template:
issuer
: the target issuer pathcontext
: the issuer context (as resulting from the issuer’sfetch_context
method)
A complete debug template URL example may look like:
http://localhost:8000/__debug__/templates/?issuer=apps.shop.issuers.invoice.InvoiceDocument&context=%7B%22invoice%22%3A+%7B%22invoice-id%22%3A+%22d972fef9%22%7D
Note that the JSON-serialized context
should be URL encoded. This can be
achieved using the following python snippet:
import json
import urllib.parse
with open("context.json") as example:
print(
urllib.parse.quote_plus(
json.dumps(
json.load(example)
)
)
)
As mentionned earlier, you should keep in mind that the media that will be used to render your document is a printer, so you should enable print media emulation in the developer tools of your web browser to have a better idea of what it will look like once rendered as a PDF.
In expected conditions (outside from a Django view context), Marion generates a
PDF file using separated HTML and CSS content. Linked files (e.g. embedded
images) are expected to be referenced using the file://
protocol (a custom
url fetcher will integrate those files in the final document). But, when using
this debug view, we need to inject CSS styles in the template and serve static
files by Django to display them in the browser. This is why we add a debug
variable to the Django context. This variable should be used to add CSS content
in the debug view:
<html>
{% if debug %}
<head>
{{ css }}
</head>
{% endif %}
<!-- [...] -->
</html>
And display images:
<img
src="{{ debug | yesno:",file://" }}{% static "shop/logo.svg" %}"
alt="{% trans "company logo" %}"
/>
Issuer configuration¶
Once written, we should declare distributed application issuers:
# apps/shop/defaults.py
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _
class ShopDocumentIssuerChoices(TextChoices):
"""List active document issuers."""
INVOICE = "apps.shop.issuers.invoice.InvoiceDocument", _("Invoice")
And activate them in our Django settings:
# pyproject/myproject/settings.py
# Add the shop app
INSTALLED_APPS = [
"django.contrib.admin",
# [...]
"rest_framework",
"marion",
"apps.shop",
]
# Activate shop issuers
MARION_DOCUMENT_ISSUER_CHOICES_CLASS = "apps.shop.defaults.ShopDocumentIssuerChoices"
Only issuers listed in the ShopDocumentIssuerChoices
can be used in the
current Django project. If you need more issuers, you should declare them in
the ShopDocumentIssuerChoices
enum-like object or declare a new enum listing
all allowed issuers for your project.
Note that modifying this setting requires to create a new database migration as this will change choices of the
DocumentRequest.issuer
field.
Document rendering¶
Once your issuer has been implemented and activated, you can generate the
corresponding PDF file using either the issuer API, the DocumentRequest
model or the REST API endpoint. In the first scenario, the generation of your
document will not be tracked as a document request in your database.
Using the issuer API¶
To generate a document, you will need to instantiate the corresponding issuer
with an appropriate context query, and then call the create()
method:
from apps.shop.issuers.invoice.InvoiceDocument
invoice = InvoiceDocument(
context_query={"order_id": "7866454a-600e-434a-a546-04a286b208db"}
)
# Generate the PDF file
invoice.create()
Your document should have been rendered in a PDF file created in the
MARION_DOCUMENTS_ROOT
setting path. For reference, see the
marion.issuers.base.AbstractDocument
class.
Using the DocumentRequest
Django model¶
If you want to track documents creation in your database, you should use
Marion’s DocumentRequest
model in your views:
# apps/shop/views.py
from marion.models import DocumentRequest
def payment(request):
"""Payment view"""
order_id = request.POST.get("order_id")
invoice = DocumentRequest.objects.create(
issuer="apps.shop.issuers.invoice.InvoiceDocument",
context_query={"order_id": order_id}
)
# [...]
Your document should have been rendered in a PDF file created in the
MARION_DOCUMENTS_ROOT
setting path. For reference, see the
marion.models.DocumentRequest
class and the
marion.issuers.base.AbstractDocument
class.
Using the REST API¶
If you have configured Marion’s urls in your project, you can use the document request view set to get, list or create a new document:
# Create a new document using the invoice issuer
$ http POST http://localhost:8000/api/documents/requests/ \
issuer="apps.shop.issuers.invoice.InvoiceDocument" \
context_query='{"order_id": "7866454a-600e-434a-a546-04a286b208db"}'
You should have a HTTP 200 OK
response. Yatta!
Once created, check the document request ID (and the corresponding document) by listing created objects via:
$ http GET http://localhost:8000/api/documents/requests/
Issuer testing¶
Don’t forget to test your business logic implemented in the fetch_context
method of your issuer. We use pytest
along with
hypothesis as it has builtin
support for Pydantic models.