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.pycontains default values to configure your issuer,issuersmodule 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/issuersdirectory 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.AbstractDocumentclass, - your issuer class should implement the 
fetch_contextmethod, - the 
fetch_contextmethod 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,
debugblocks 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_contextmethod)
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.issuerfield.
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.