Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
shinneider committed Nov 6, 2021
0 parents commit 4aa722e
Show file tree
Hide file tree
Showing 16 changed files with 578 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# IDE Folders
/.idea/*
/.vscode/*

# Python Files
*.pyc

# SQL Files
*.db
*.sqlite3
*.sql

# Test's
/htmlcov/*
.coverage/
.coverage*
coverage.xml
pylint.txt

# SonarQube
.scannerwork

# Others
/venv/*
/dist/*
/drf_pdf_renderer.egg-info/*
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The MIT License (MIT)

Copyright (c) 2021 Shinneider

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to
do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 changes: 9 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
include LICENSE
include README.md

recursive-include drf_pdf_renderer/templates *
recursive-include drf_pdf_renderer/static *
recursive-include drf_pdf_renderer/locale *
global-exclude *.py[co]
prune __pycache__
prune test
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
DRF PDF Renderer
=
A simplistic/very extendable pdf renderer.

If you use or like the project, click `Star` and `Watch` to generate metrics and i evaluate project continuity.

OBS
=
This project is a `Beta` version, however is used in production in a big project (with custom pdf template, and manually fields instead of automatic).
however, due to my low availability, updates may take some time.
but i will keep an eye on the PR.

# Install:
pip install drf-pdf-renderer

# Usage:
1. Add to your `INSTALLED_APPS`, in `settings.py`:
```
INSTALLED_APPS = [
...
'drf_pdf_renderer',
...
]
```

1. In your file:
```
from rest_framework.settings import api_settings
from drf_pdf_renderer.renderer import PDFRendererPaginated
class YourView(...)
renderer_classes = (*api_settings.DEFAULT_RENDERER_CLASSES, PDFRendererPaginated)
pdf_display_fields = (['id', 'Label for ID'], ) # used only in automatic field (caution: refactor planned in futures versions)
pdf_display_fields = '' # contains two built-in templates ['pdf/list_landscape.html', 'pdf/list_portrait.html']
...
...
1. Mixin for paginated results
- if you have a pagination on DRF, but require a PDF with all registries, you can use this Mixin
```
from rest_framework.settings import api_settings
from drf_pdf_renderer.mixin import PdfAllResultsMixin

class YourView(PdfAllResultsMixin, ...)
pdf_display_fields = (['id', 'Label for ID'], ) # used only in automatic field (caution: refactor planned in futures versions)
pdf_display_fields = '' # contains two built-in templates ['pdf/list_landscape.html', 'pdf/list_portrait.html']
...
...

# Advanced
1. Custom PDF Template
- this project use [xhtml2pdf](https://github.com/xhtml2pdf/xhtml2pdf), check documentation of html constructor [here](https://xhtml2pdf.readthedocs.io/en/latest/format_html.html).

1. Changing PDF title
```
# First way
class YourView(...)
pdf_title = 'My Title'
# Second way
class YourView(...)
def pdf_get_title(self, data, context)
return ''
```

1. Changing PDF download name
```
# First way
class YourView(...)
pdf_filename = 'My Title'
# Second way
class YourView(...)
def pdf_get_filename(self, pdf, data)
return ''
```

1. Custom data to render context
```
- By default `data`, `request`, `title` and `fields` will always be present (but can be rewrited)
# Second way
class YourView(...)
def pdf_mount_context(data)
return {'adm': True}
```
10 changes: 10 additions & 0 deletions drf_pdf_renderer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
try:
import django
except ImportError:
django = None

__version__ = '0.1.0'

if django and django.VERSION < (3, 2): # pragma: no cover
default_app_config = 'drf_pdf_renderer.apps.DjangoPdfRendererConfig'
8 changes: 8 additions & 0 deletions drf_pdf_renderer/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _


class DjangoPdfRendererConfig(AppConfig): # Our app config class
name = 'django_admin_search'
verbose_name = _('Django Admin Search')
12 changes: 12 additions & 0 deletions drf_pdf_renderer/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework.settings import api_settings

from drf_pdf_renderer.renderer import PDFRendererPaginated


class PdfAllResultsMixin:
renderer_classes = (*api_settings.DEFAULT_RENDERER_CLASSES, PDFRendererPaginated)

def paginate_queryset(self, queryset):
if self.paginator and self.request.accepted_renderer.format == "pdf":
self.paginator.page_size = 99999
return super().paginate_queryset(queryset)
103 changes: 103 additions & 0 deletions drf_pdf_renderer/renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import re

from django.http import HttpResponse
from django.template.loader import render_to_string
from rest_framework.renderers import BaseRenderer
from xhtml2pdf import pisa

from drf_pdf_renderer.utils import link_callback


class MountPdfMixin:

def get_template(self):
if hasattr(self.context['view'], 'pdf_get_renderer_template'):
return self.context['view'].pdf_get_renderer_template(self.data)
elif hasattr(self.context['view'], 'pdf_renderer_template'):
return self.context['view'].pdf_renderer_template
return 'pdf/list_portrait.html'

def mount_show_fields(self):
if hasattr(self.context['view'], 'pdf_get_display_fields'):
return self.context['view'].pdf_get_display_fields(self.data)
elif hasattr(self.context['view'], 'pdf_display_fields'):
return self.context['view'].pdf_display_fields

return []

def get_title(self):
if hasattr(self.context['view'], 'pdf_get_title'):
return self.context['view'].pdf_get_title(self.data)
elif hasattr(self.context['view'], 'pdf_title'):
return self.context['view'].pdf_title

return re.sub(r"(\w)([A-Z])", r"\1 \2", self.context['view'].__class__.__name__)

def mount_context(self):
base_context = {
'data': self.data,
'request': self.context['request'],
**self.get_additional_context()
}

if hasattr(self.context['view'], 'pdf_mount_context'):
return {
**self.context['view'].pdf_mount_context(self.data),
**base_context
}

return base_context

def get_additional_context(self):
return {
'fields': self.mount_show_fields(),
'title': self.get_title()
}

def render_template(self):
return render_to_string(template_name=self.get_template(), context=self.mount_context())

@staticmethod
def render_pdf(template):
return pisa.CreatePDF(template, link_callback=link_callback).dest


class PDFRenderer(MountPdfMixin, BaseRenderer):
media_type = 'application/pdf'
format = 'pdf'

def get_pdf_filename(self, pdf):
if hasattr(self.context['view'], 'pdf_get_filename'):
return self.context['view'].pdf_get_filename(pdf, self.data)
elif hasattr(self.context['view'], 'pdf_filename'):
return self.context['view'].pdf_filename

return 'report.pdf'

def mount_response(self, pdf):
filename = self.get_pdf_filename(pdf)
response = HttpResponse(pdf.getvalue(), content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename={filename}'
return response

def process_render(self):
if self.data is None or self.context['response'].status_code >= 300:
return self.data

template = self.render_template()
pdf = self.render_pdf(template)
return self.mount_response(pdf)

def render(self, data, accepted_media_type=None, renderer_context=None):
self.data = data
self.context = renderer_context
return self.process_render()


class PDFRendererPaginated(PDFRenderer):
results_field = 'results'

def render(self, data, *args, **kwargs):
if not isinstance(data, list):
data = data.get(self.results_field, [])
return super().render(data, *args, **kwargs)
50 changes: 50 additions & 0 deletions drf_pdf_renderer/templates/pdf/base_landscape.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
{% block page_definition %}
@page {
size: a4 landscape;
@frame header_frame {
-pdf-frame-content: header_content;
width: 832pt;
height: 40pt;
top: 0pt;
margin: 5pt 5pt 0pt 5pt;
}
@frame content_frame {
width: 832pt;
top: 40pt;
height: 520pt;
margin: 0pt 5pt 0pt 5pt;
}
@frame footer_frame {
-pdf-frame-content: footer_content;
width: 832pt;
top: 570pt;
height: 20pt;
margin: 0pt 5pt 5pt 5pt;
}
{% block custom_page_attrs %}{% endblock %}
}
{% endblock %}
{% block custom_styles %}{% endblock %}
</style>
</head>

<body>
<!-- Content for Static Frame 'header_frame' -->
<div id="header_content">{% block header %}{% endblock %}</div>

<!-- Content for Static Frame 'footer_frame' -->
<div id="footer_content">
{% block footer %}
Page <pdf:pagenumber> of <pdf:pagecount>
{% endblock %}
</div>

<!-- HTML Content -->
{% block content %}{% endblock %}

</body>
</html>
50 changes: 50 additions & 0 deletions drf_pdf_renderer/templates/pdf/base_portrait.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
{% block page_definition %}
@page {
size: a4 portrait;
@frame header_frame { /* Static Frame */
-pdf-frame-content: header_content;
width: 585pt;
height: 40pt;
top: 0pt;
margin: 5pt 5pt 0pt 5pt;
}
@frame content_frame { /* Content Frame */
width: 585pt;
top: 40pt;
height: 770pt;
margin: 0pt 5pt 0pt 5pt;
}
@frame footer_frame { /* Another static Frame */
-pdf-frame-content: footer_content;
width: 585pt;
top: 820pt;
height: 20pt;
margin: 0pt 5pt 5pt 5pt;
}
{% block custom_page_attrs %}{% endblock %}
}
{% endblock %}
{% block custom_styles %}{% endblock %}
</style>
</head>

<body>
<!-- Content for Static Frame 'header_frame' -->
<div id="header_content">{% block header %}{% endblock %}</div>

<!-- Content for Static Frame 'footer_frame' -->
<div id="footer_content">
{% block footer %}
Page <pdf:pagenumber> of <pdf:pagecount>
{% endblock %}
</div>

<!-- HTML Content -->
{% block content %}{% endblock %}

</body>
</html>
Loading

0 comments on commit 4aa722e

Please sign in to comment.