├── pdfutils
├── models.py
├── static
│ └── pdfutils
│ │ └── css
│ │ ├── base.css
│ │ ├── landscape.css
│ │ └── portrait.css
├── __init__.py
├── utils.py
├── reports.py
└── sites.py
├── .gitignore
├── setup.py
├── do-release
├── README.rst
├── changelog
└── .gitchangelog.rc
/pdfutils/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.pot
3 | *.pyc
4 | *~
5 | *.egg-info
6 | venv
7 | dist
8 | build
9 | local_settings.py
10 |
--------------------------------------------------------------------------------
/pdfutils/static/pdfutils/css/base.css:
--------------------------------------------------------------------------------
1 | html {
2 |
3 | }
4 |
5 | h1 {
6 | text-align: center;
7 | font-size: 32px;
8 | }
9 |
--------------------------------------------------------------------------------
/pdfutils/static/pdfutils/css/landscape.css:
--------------------------------------------------------------------------------
1 | @page {
2 | size: letter landscape;
3 | margin: 2.5cm 1cm;
4 | @frame header {
5 | -pdf-frame-content: header;
6 | top: 0.5cm;
7 | left: 1cm;
8 | right: 1cm;
9 | height: 2cm;
10 | }
11 |
12 | @frame footer {
13 | -pdf-frame-content: footer;
14 | bottom: 1.5cm;
15 | margin-left: 1cm;
16 | margin-right: 1cm;
17 | height: 1.5cm;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pdfutils/static/pdfutils/css/portrait.css:
--------------------------------------------------------------------------------
1 | @page {
2 | size: letter portrait;
3 | margin: 2.5cm 1cm;
4 | h3 { background: red; }
5 | @frame header {
6 | -pdf-frame-content: header;
7 | top: 0.5cm;
8 | left: 1cm;
9 | right: 1cm;
10 | height: 2cm;
11 | }
12 |
13 | @frame content {
14 | -pdf-frame-content: content;
15 | top: 2.5cm;
16 | left: 1cm;
17 | right: 1cm;
18 | height: 2cm;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 |
5 | from setuptools import setup, find_packages
6 |
7 | def read(fname):
8 | return open(os.path.join(os.path.dirname(__file__), fname)).read()
9 |
10 | setup(
11 | name='PDFutils',
12 | version='1.1.3',
13 | description='Django PDFutils',
14 | long_description=(read('README.rst')),
15 | author='Maxime Haineault',
16 | author_email='haineault@gmail.com',
17 | license='BSD',
18 | url='https://github.com/h3/django-pdfutils',
19 | packages=find_packages(),
20 | include_package_data=True,
21 | package_data={'pdfutils': [
22 | 'README.rst',
23 | 'changelog',
24 | ]},
25 | install_requires = [
26 | 'django>=1.4',
27 | 'decorator==3.4.0,<=4.0.2',
28 | 'reportlab==2.5',
29 | 'html5lib==0.90',
30 | 'httplib2==0.9',
31 | 'pyPdf==1.13',
32 | 'xhtml2pdf==0.0.4',
33 | ],
34 | zip_safe=False,
35 | classifiers=[
36 | 'Development Status :: 3 - Alpha',
37 | 'Environment :: Web Environment',
38 | 'Intended Audience :: Developers',
39 | 'License :: OSI Approved :: BSD License',
40 | 'Operating System :: OS Independent',
41 | 'Programming Language :: Python',
42 | 'Framework :: Django',
43 | ]
44 | )
45 |
--------------------------------------------------------------------------------
/pdfutils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Taken largely from django.contrib.admin
3 | """
4 |
5 | from pdfutils.sites import ReportSite, site
6 |
7 |
8 | def autodiscover():
9 | """
10 | Auto-discover INSTALLED_APPS report.py modules and fail silently when
11 | not present. This forces an import on them to register any report bits they
12 | may want.
13 | """
14 |
15 | import copy
16 | from django.conf import settings
17 | from django.utils.importlib import import_module
18 | from django.utils.module_loading import module_has_submodule
19 |
20 | for app in settings.INSTALLED_APPS:
21 | mod = import_module(app)
22 | # Attempt to import the app's admin module.
23 | try:
24 | before_import_registry = copy.copy(site._registry)
25 | import_module('%s.report' % app)
26 | except:
27 | # Reset the model registry to the state before the last import as
28 | # this import will have to reoccur on the next request and this
29 | # could raise NotRegistered and AlreadyRegistered exceptions
30 | # (see #8245).
31 | site._registry = before_import_registry
32 |
33 | # Decide whether to bubble up this error. If the app just
34 | # doesn't have an admin module, we can ignore the error
35 | # attempting to import it, otherwise we want it to bubble up.
36 | if module_has_submodule(mod, 'report'):
37 | raise
38 |
--------------------------------------------------------------------------------
/pdfutils/utils.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | import os
3 | import StringIO
4 |
5 | from django.conf import settings
6 | from django.template.context import Context
7 | from django.template.loader import get_template
8 |
9 | from xhtml2pdf import pisa # TODO: Change this when the lib changes.
10 |
11 | """
12 |
13 | The code below is taken from django-xhtml2pdf
14 |
15 | https://raw.github.com/chrisglass/django-xhtml2pdf/master/django_xhtml2pdf/utils.py
16 |
17 | """
18 |
19 |
20 | class UnsupportedMediaPathException(Exception):
21 | pass
22 |
23 |
24 | def unique(seq):
25 | seen = set()
26 | seen_add = seen.add
27 | return [ x for x in seq if not (x in seen or seen_add(x))]
28 |
29 |
30 | def fetch_resources(uri, rel):
31 | """
32 | Callback to allow xhtml2pdf/reportlab to retrieve Images,Stylesheets, etc.
33 | `uri` is the href attribute from the html link element.
34 | `rel` gives a relative path, but it's not used here.
35 | """
36 | if uri.startswith('http://') or uri.startswith('https://'):
37 | return uri
38 | if uri.startswith(settings.MEDIA_URL):
39 | path = os.path.join(settings.MEDIA_ROOT,
40 | uri.replace(settings.MEDIA_URL, ""))
41 | elif uri.startswith(settings.STATIC_URL):
42 | path = os.path.join(settings.STATIC_ROOT,
43 | uri.replace(settings.STATIC_URL, ""))
44 | if not os.path.exists(path):
45 | for d in settings.STATICFILES_DIRS:
46 | path = os.path.join(d, uri.replace(settings.STATIC_URL, ""))
47 | if os.path.exists(path):
48 | break
49 | else:
50 | raise UnsupportedMediaPathException(
51 | 'media urls must start with %s or %s' % (
52 | settings.MEDIA_ROOT, settings.STATIC_ROOT))
53 | return path
54 |
55 |
56 | def generate_pdf_template_object(template_object, file_object, context):
57 | """
58 | Inner function to pass template objects directly instead of passing a filename
59 | """
60 | html = template_object.render(Context(context))
61 | pisaStatus = pisa.CreatePDF(html.encode("UTF-8"), file_object , encoding='UTF-8')
62 | file_object.close()
63 | return pisaStatus.err
64 |
65 |
66 |
67 | def generate_pdf(template_name, file_object=None, context=None): # pragma: no cover
68 | """
69 | Uses the xhtml2pdf library to render a PDF to the passed file_object, from the
70 | given template name.
71 |
72 | This returns the passed-in file object, filled with the actual PDF data.
73 | In case the passed in file object is none, it will return a StringIO instance.
74 | """
75 | if not file_object:
76 | file_object = StringIO.StringIO()
77 | if not context:
78 | context = {}
79 | tmpl = get_template(template_name)
80 | pisaStatus = generate_pdf_template_object(tmpl, file_object, context)
81 | file_object.close()
82 | return pisaStatus
83 |
--------------------------------------------------------------------------------
/do-release:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Returns 1 if the current git branch is dirty, else 0
4 | function git_is_dirty {
5 | [[ $(git diff --shortstat 2> /dev/null | tail -n1) != "" ]] && echo 1 || echo 0
6 | }
7 |
8 | # Returns the number of untracked files
9 | function git_has_untracked_files {
10 | expr `git status --porcelain 2>/dev/null| grep "^??" | wc -l`
11 | }
12 |
13 | function get_next_version {
14 | $MAJOR=$1
15 | $MINOR=$2
16 | $REV=$(expr $3 + 1)
17 | echo "$MAJOR.$MINOR.$REV"
18 | echo "$MAJOR.$MINOR.$REV"
19 | }
20 |
21 | CURRENT_VERSION=$(grep "version='[0-9].[0-9].[0-9]'" setup.py | grep -o '[0-9].[0-9].[0-9]')
22 | CV=( ${CURRENT_VERSION//./ } )
23 | CURRENT_MAJOR=${CV[0]}
24 | CURRENT_MINOR=${CV[1]}
25 | CURRENT_REV=${CV[2]}
26 |
27 | NEXT_MAJOR=$CURRENT_MAJOR
28 | NEXT_MINOR=$CURRENT_MINOR
29 | NEXT_REV=`expr $CURRENT_REV + 1`
30 | if [ $NEXT_REV -gt 9 ]; then
31 | NEXT_MINOR=`expr $NEXT_MINOR + 1`
32 | NEXT_REV=0
33 | if [ $NEXT_MINOR -gt 9 ]; then
34 | NEXT_MINOR=0
35 | NEXT_MAJOR=`expr $NEXT_MAJOR + 1`
36 | fi
37 | fi
38 | NEXT_VERSION="$NEXT_MAJOR.$NEXT_MINOR.$NEXT_REV"
39 | BRANCH="release_${NEXT_MAJOR}_${NEXT_MINOR}"
40 |
41 | echo "You are about to build and publish version $NEXT_VERSION"
42 | read -sn 1 -p "Press any key to continue..."
43 | echo
44 |
45 | # Make sure we are in sync with remote
46 | git checkout -q master && git fetch -q && git pull -q
47 |
48 | # Verify that there is nothing to commit
49 | if [ $(git_is_dirty) -eq 1 ]; then
50 | git status
51 | echo
52 | echo -e " [error] Hey dumbass, commit your shit.. Abort !\n\n"
53 | #exit 1
54 | fi
55 |
56 | # Verify that there is no untracked files
57 | if [ $(git_has_untracked_files) -gt 0 ]; then
58 | git status
59 | echo
60 | echo -e " [error] I see untracked files, you dirty bastard.. Abort !\n\n"
61 | exit 1
62 | fi
63 |
64 | echo " [info] Bumping version"
65 | sed -i -e "s/ version='"$CURRENT_VERSION"',/ version='"$NEXT_VERSION"',/" setup.py
66 | git commit -aqm "Version bump: $CURRENT_VERSION -> $NEXT_VERSION @wip"
67 | git push -q
68 |
69 | echo " [info] Creating tag v$NEXT_VERSION"
70 | git tag -s "v$NEXT_VERSION" -m "Release $NEXT_VERSION"
71 | git push --tags origin "v$NEXT_VERSION"
72 |
73 | echo " [info] Updating changelog"
74 | if [ ! -d "./venv" ]; then
75 | virtualenv venv
76 | fi
77 | ./venv/bin/pip install -q gitchangelog
78 | ./venv/bin/gitchangelog > changelog
79 | rm -rf venv
80 | git add changelog
81 | git commit -qm "Updated changelog @wip"
82 | git push -q
83 |
84 | # check if a git branch exists for the current major_minor release (ex: release_1_0)
85 | if [ `git branch --list $BRANCH` ]; then # if not, create one.
86 | echo " [info] UPDATING release branch $BRANCH"
87 | git checkout -q $BRANCH
88 | git merge -q origin/master
89 | else
90 | echo " [info] CREATING release branch $BRANCH"
91 | git checkout -q -b $BRANCH
92 | fi
93 | git push -q --set-upstream origin $BRANCH > /dev/null
94 | git checkout -q master
95 |
96 | echo " [info] Pushing to PyPi"
97 | python setup.py sdist build upload
98 |
99 | echo " [done] Published release v$NEXT_VERSION"
100 | echo
101 | exit 0
102 |
--------------------------------------------------------------------------------
/pdfutils/reports.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import posixpath
5 | import sys
6 | import tempfile
7 |
8 | try:
9 | from urllib.parse import unquote
10 | except ImportError: # Python 2
11 | from urllib import unquote
12 |
13 | from django.conf import settings
14 | from django.contrib.staticfiles import finders
15 | from django.http import HttpResponse
16 | from django.shortcuts import render_to_response
17 | from django.views.generic import TemplateView
18 |
19 | from pdfutils.utils import generate_pdf, unique
20 |
21 |
22 | class ReportBase(TemplateView):
23 | title = u'Untitled report'
24 | orientation = 'portrait'
25 |
26 | def __init__(self):
27 | self.__styles = ['pdfutils/css/base.css']
28 |
29 | def filename(self):
30 | return 'Untitled-document.pdf'
31 |
32 | def get_format(self):
33 | return self.request.GET.get('format', 'pdf')
34 |
35 | def get_context_data(self):
36 | self.context = {
37 | 'title': self.title,
38 | 'slug': self.slug,
39 | 'user': self.request.user,
40 | 'orientation': self.orientation,
41 | 'MEDIA_URL': settings.MEDIA_URL,
42 | 'STATIC_URL': settings.STATIC_URL,
43 | 'STYLES': self.render_styles(),
44 | }
45 | return self.context
46 |
47 | def add_styles(self, styles):
48 | self.__styles = []
49 |
50 | for style in styles:
51 | self.__styles.append(style)
52 |
53 | def get_styles(self):
54 | if self.orientation == 'portrait':
55 | self.__styles.append('pdfutils/css/portrait.css')
56 | else:
57 | self.__styles.append('pdfutils/css/landscape.css')
58 | self.__styles = unique(self.__styles)
59 | return self.__styles
60 |
61 | def render_styles(self):
62 | """
63 | Eventually this should return a list of tags
64 | instead of inline styles. xhtml2pdf has a weird bug
65 | which prevents external stylesheet from working.
66 | """
67 | out = []
68 | for style in self.get_styles():
69 | path = style
70 | normalized_path = posixpath.normpath(unquote(path)).lstrip('/')
71 | absolute_path = finders.find(normalized_path)
72 | if absolute_path:
73 | with open (absolute_path, "r") as fd:
74 | out.append(fd.read())
75 | else:
76 | print "[pdfutils error] File not found: %s" % style
77 | return '' % ''.join(out)
78 |
79 | def render_to_file(self):
80 | """
81 | Renders a PDF report to a temporary file
82 | """
83 | return generate_pdf(self.template_name, context=self.get_context_data())
84 |
85 | def render(self):
86 | """
87 | Renders a PDF report to the HttpRequest object
88 | """
89 | ctx = self.get_context_data()
90 | self.response = HttpResponse(mimetype='application/pdf', \
91 | content_type='application/pdf; name=%s' % self.filename())
92 |
93 | generate_pdf(self.template_name, \
94 | file_object=self.response, context=ctx)
95 |
96 | self.response['Content-Disposition'] = 'inline; filename=%s' % \
97 | self.filename()
98 |
99 | return self.response
100 |
101 | def get(self, request, *args, **kwargs):
102 | if self.get_format() == 'pdf':
103 | return self.render()
104 | return super(ReportBase, self).get(request, *args, **kwargs)
105 |
106 |
107 | class Report(ReportBase):
108 | pass
109 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | django-pdfutils
2 | ===============
3 |
4 | A simple django app to generate PDF documents.
5 |
6 |
7 | Installation
8 | ------------
9 |
10 | 1. In your `settings.py`, add `pdfutils` to your `INSTALLED_APPS`.
11 | 2. `(r'^reports/', include(pdfutils.site.urls)),` to your `urls.py`
12 | 3. Add `pdfutils.autodiscover()` to your `urls.py`
13 | 4. Create a `report.py` file in any installed django application.
14 | 5. Create your report(s)
15 | 6. Profit!
16 |
17 | **Note**: If you are using buildout, don't forget to put `pdfutils`
18 | in your `eggs` section or else the django-pdfutils dependencies wont
19 | be installed.
20 |
21 |
22 | Example report
23 | --------------
24 |
25 | Reports are basically views with custom methods and properties.
26 |
27 | .. code-block:: python
28 |
29 | # -*- coding: utf-8 -*-
30 |
31 | from django.contrib.auth.models import User
32 | from django.core.urlresolvers import reverse
33 | from django.utils.translation import ugettext as _
34 |
35 | from pdfutils.reports import Report
36 | from pdfutils.sites import site
37 |
38 |
39 | class MyUserReport(Report):
40 | title = _('Users')
41 | template_name = 'myapp/reports/users-report.html'
42 | slug = 'users-report'
43 | orientation = 'portrait'
44 |
45 | def get_users(self):
46 | return User.objects.filter(is_staff=True)
47 |
48 | def get_styles(self):
49 | """
50 | It is possible to add or override style like so
51 | """
52 | self.add_styles('myapp/css/users-report.css')
53 | return super(AccountStatementReport, self).get_styles()
54 |
55 | def filename(self):
56 | """
57 | The filename can be generated dynamically and translated
58 | """
59 | return _('Users-report-%(count)s.pdf') % {'count': self.get_users().count() }
60 |
61 | def get_context_data(self):
62 | """
63 | Context data is injected just like a normal view
64 | """
65 | context = super(AccountStatementReport, self).get_context_data()
66 | context['user_list'] = self.get_users()
67 | return context
68 |
69 | site.register(MyUserReport)
70 |
71 |
72 | The slug should obviously be unique since it is used to build the report URL.
73 |
74 | For example, with the default settings and URLs, the URL for report above would be `/reports/users-report/`.
75 |
76 | Example template
77 | ----------------
78 |
79 | .. code-block:: html
80 |
81 |
82 |
83 | {{ STYLES|safe }}
84 |
85 |
86 |
87 | {% for user in user_list %}
88 |
{{ user }}
89 | {% endfor %}
90 |
91 | Add ?format=html for easy template debug
92 |
93 |
94 |
95 | Some template variables are injected by default in reports:
96 |
97 | * title
98 | * slug
99 | * orientation
100 | * MEDIA_URL
101 | * STATIC_URL
102 | * STYLES
103 |
104 |
105 | Overriding default CSS
106 | ----------------------
107 |
108 | Since the default CSS (base.css, portrait.css, landscape.css) are normal static files, they can be overrided
109 | from any other django app which has a `pdfutils` folder in their static folder.
110 |
111 | Note: Be sure your applications are listed in the right order in `INSTALLED_APPS` !
112 |
113 |
114 | Dependencies
115 | ------------
116 |
117 | * django >=1.4, < 1.5.99
118 | * decorator == 3.4.0, <= 3.9.9
119 | * PIL == 1.1.7
120 | * reportlab == 2.5
121 | * html5lib == 0.90
122 | * httplib2 == 0.9
123 | * pyPdf == 1.13
124 | * xhtml2pdf == 0.0.4
125 | * django-xhtml2pdf == 0.0.3
126 |
127 | **Note**: dependencies versions are specified in `setup.py`. The amount of time required to find the right
128 | combination of dependency versions is largely to blame for the creation of this project.
129 |
--------------------------------------------------------------------------------
/changelog:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | v1.1.3 (2015-04-11)
5 | -------------------
6 |
7 | - Merge pull request #7 from dominicmontreuil/patch-4. [Maxime
8 | Haineault]
9 |
10 | Letting base manage styles
11 |
12 | - Letting base manage styles. [Dominic Montreuil]
13 |
14 | Mangling self.styles to self.__styles should let classes subclassing ReportBase know that you shouldn't add to styles directly and let the base class manage them.
15 |
16 | The reason in this case is to let ReportBase handle the styles and work around the threadlock issues by cleaning the styles prior to a new report being executed. Doing so has the effect of minimizing work in the utils.unique method making sure that before each report execution we have a minimum or more explicitely the relevant styles for our current report.
17 |
18 | Classes subclassing ReportBase should use the append_styles(self, styles) method instead of appending to styles directly.
19 |
20 | - Merge pull request #9 from dominicmontreuil/patch-3. [Maxime
21 | Haineault]
22 |
23 | Better practice in utils.py
24 |
25 | - Better practice in utils.py. [Dominic Montreuil]
26 |
27 | The link_callback wasn't really used...although it didn't cause harm either.
28 |
29 | generate_pdf and generate_pdf_template_object didn't return anything useful and whatever they returned wasn't used anyways. Pisa documentation recommends that such functions/methods return the generation errors for exception catching. Not implemented here, but just an overall better practice.
30 |
31 | Also is the addition of file_object.close(). You should manually close your StringIO objects as in some reported errors not closing them was the cause for massive slowdowns and hungry ressource usage by xhtml2pdf.
32 |
33 | All the recommended changes here are optional, but the StringIO manual close is recommended very highly to Pisa/xhtml2pdf users.
34 |
35 | v1.0.9 (2015-03-28)
36 | -------------------
37 |
38 | - Fixed setup.py. [Maxime Haineault]
39 |
40 | v1.0.8 (2015-03-28)
41 | -------------------
42 |
43 | - Fixed important memory leak in style loading (thanks Dom) [Maxime
44 | Haineault]
45 |
46 | v1.0.7 (2015-03-28)
47 | -------------------
48 |
49 | - Merge pull request #3 from dulaccc/format-filter. [Maxime Haineault]
50 |
51 | Add a format filter to simplify debugging
52 |
53 | - Add a filter using GET parameters to simplify debugging. [Pierre
54 | Dulac]
55 |
56 | - Add autodiscover step to the readme file. [Pierre Dulac]
57 |
58 | - Merge branch 'dulaccc-add-url-name' [Maxime Haineault]
59 |
60 | - Add a url name property to the urlpattern generated. [Maxime
61 | Haineault]
62 |
63 | - Add a url name property to the urlpattern generated. [Pierre Dulac]
64 |
65 | - Merge branch 'dulaccc-fix-resource-remote-url' [Maxime Haineault]
66 |
67 | - .. [Maxime Haineault]
68 |
69 | - Let pisa handle remote resources. [Pierre Dulac]
70 |
71 | - Merge pull request #6 from dominicmontreuil/patch-1. [Maxime
72 | Haineault]
73 |
74 | Update setup.py @wip
75 |
76 | - Update setup.py. [Dominic Montreuil]
77 |
78 | I needed to use pdfutils along with another package that required httplib2 0.9 or higher.
79 |
80 | I tested pdfutils with httplib2 0.9 and everything works. It should be safe to update to the newer version of httplib2.
81 |
82 | - Cleaning up venv after build. [Maxime Haineault]
83 |
84 | v1.0.6 (2015-03-27)
85 | -------------------
86 |
87 | - Cleanup. [Maxime Haineault]
88 |
89 | v1.0.5 (2015-03-27)
90 | -------------------
91 |
92 | - Fixed build script. [Maxime Haineault]
93 |
94 | v1.0.4 (2015-03-27)
95 | -------------------
96 |
97 | - Let pisa handle remote resources. [Maxime Haineault]
98 |
99 | https://github.com/dulaccc/django-pdfutils/commit/e92304c2c952a902c6461787aec2cb269595b738
100 |
101 | - Add a url name property to the urlpattern generated. [Maxime
102 | Haineault]
103 |
104 | https://github.com/dulaccc/django-pdfutils/commit/161c7fc044d21dbaf91c57266f4b408846af8122
105 |
106 | - Updated to httplib2 0.9. [Maxime Haineault]
107 |
108 | - Messed up version, dammit. [Maxime Haineault]
109 |
110 | v1.0.3 (2015-03-27)
111 | -------------------
112 |
113 | - Wheel doesn't build. well.. no wheel. [Maxime Haineault]
114 |
115 | v1.0.2 (2015-03-27)
116 | -------------------
117 |
118 | - Updated .gitignore. [Maxime Haineault]
119 |
120 | - Updated build script, now building wheel. [Maxime Haineault]
121 |
122 | v1.0.1 (2015-03-27)
123 | -------------------
124 |
125 | - Updated .gitignore, almost finished build script, updated setup.py.
126 | [Maxime Haineault]
127 |
128 | - Added a release script. [Maxime Haineault]
129 |
130 | - Merged. [Maxime Haineault]
131 |
132 | - Update setup.py. [Maxime Haineault]
133 |
134 | - Updated urls for django 1.5+ (unused), updated gitignore. [Maxime
135 | Haineault]
136 |
137 | - Fixed small bug in get_context. [Maxime Haineault]
138 |
139 | - Removed maximum django version restriction, updated gitignore. [Maxime
140 | Haineault]
141 |
142 | - Removed PIL dependency. [Maxime Haineault]
143 |
144 | - Removed django-xhtml2pdf dependency. [Maxime Haineault]
145 |
146 | - Removed THUMBNAIL_MEDIA_URL from template variables. [Maxime
147 | Haineault]
148 |
149 |
150 |
--------------------------------------------------------------------------------
/.gitchangelog.rc:
--------------------------------------------------------------------------------
1 | ##
2 | ## Format
3 | ##
4 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
5 | ##
6 | ## Description
7 | ##
8 | ## ACTION is one of 'chg', 'fix', 'new'
9 | ##
10 | ## Is WHAT the change is about.
11 | ##
12 | ## 'chg' is for refactor, small improvement, cosmetic changes...
13 | ## 'fix' is for bug fixes
14 | ## 'new' is for new features, big improvement
15 | ##
16 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
17 | ##
18 | ## Is WHO is concerned by the change.
19 | ##
20 | ## 'dev' is for developpers (API changes, refactors...)
21 | ## 'usr' is for final users (UI changes)
22 | ## 'pkg' is for packagers (packaging changes)
23 | ## 'test' is for testers (test only related changes)
24 | ## 'doc' is for doc guys (doc only changes)
25 | ##
26 | ## COMMIT_MSG is ... well ... the commit message itself.
27 | ##
28 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
29 | ##
30 | ## They are preceded with a '!' or a '@' (prefer the former, as the
31 | ## latter is wrongly interpreted in github.) Commonly used tags are:
32 | ##
33 | ## 'refactor' is obviously for refactoring code only
34 | ## 'minor' is for a very meaningless change (a typo, adding a comment)
35 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
36 | ## 'wip' is for partial functionality but complete subfunctionality.
37 | ##
38 | ## Example:
39 | ##
40 | ## new: usr: support of bazaar implemented
41 | ## chg: re-indentend some lines !cosmetic
42 | ## new: dev: updated code to be compatible with last version of killer lib.
43 | ## fix: pkg: updated year of licence coverage.
44 | ## new: test: added a bunch of test around user usability of feature X.
45 | ## fix: typo in spelling my name in comment. !minor
46 | ##
47 | ## Please note that multi-line commit message are supported, and only the
48 | ## first line will be considered as the "summary" of the commit message. So
49 | ## tags, and other rules only applies to the summary. The body of the commit
50 | ## message will be displayed in the changelog without reformatting.
51 |
52 |
53 | ##
54 | ## ``ignore_regexps`` is a line of regexps
55 | ##
56 | ## Any commit having its full commit message matching any regexp listed here
57 | ## will be ignored and won't be reported in the changelog.
58 | ##
59 | ignore_regexps = [
60 | r'@minor', r'!minor',
61 | r'@cosmetic', r'!cosmetic',
62 | r'@refactor', r'!refactor',
63 | r'@wip', r'!wip', '.*@(wip|refactor|minor|cosmetic).*',
64 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
65 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
66 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
67 | r'^Version bump.*',
68 | r'^.*README.*',
69 | r'^Initial .*',
70 | r'.*swap file$',
71 | r'.*swap files$',
72 | ]
73 |
74 |
75 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a
76 | ## list of regexp
77 | ##
78 | ## Commit messages will be classified in sections thanks to this. Section
79 | ## titles are the label, and a commit is classified under this section if any
80 | ## of the regexps associated is matching.
81 | ##
82 | section_regexps = [
83 | ('New', [
84 | r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
85 | ]),
86 | ('Changes', [
87 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
88 | ]),
89 | ('Fix', [
90 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
91 | ]),
92 |
93 | ('Other', None ## Match all lines
94 | ),
95 |
96 | ]
97 |
98 |
99 | ## ``body_process`` is a callable
100 | ##
101 | ## This callable will be given the original body and result will
102 | ## be used in the changelog.
103 | ##
104 | ## Available constructs are:
105 | ##
106 | ## - any python callable that take one txt argument and return txt argument.
107 | ##
108 | ## - ReSub(pattern, replacement): will apply regexp substitution.
109 | ##
110 | ## - Indent(chars=" "): will indent the text with the prefix
111 | ## Please remember that template engines gets also to modify the text and
112 | ## will usually indent themselves the text if needed.
113 | ##
114 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
115 | ##
116 | ## - noop: do nothing
117 | ##
118 | ## - ucfirst: ensure the first letter is uppercase.
119 | ## (usually used in the ``subject_process`` pipeline)
120 | ##
121 | ## - final_dot: ensure text finishes with a dot
122 | ## (usually used in the ``subject_process`` pipeline)
123 | ##
124 | ## - strip: remove any spaces before or after the content of the string
125 | ##
126 | ## Additionally, you can `pipe` the provided filters, for instance:
127 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
128 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
129 | #body_process = noop
130 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
131 |
132 |
133 | ## ``subject_process`` is a callable
134 | ##
135 | ## This callable will be given the original subject and result will
136 | ## be used in the changelog.
137 | ##
138 | ## Available constructs are those listed in ``body_process`` doc.
139 | subject_process = (strip |
140 | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
141 | ucfirst | final_dot)
142 |
143 |
144 | ## ``tag_filter_regexp`` is a regexp
145 | ##
146 | ## Tags that will be used for the changelog must match this regexp.
147 | ##
148 | tag_filter_regexp = r'^v[0-9]+\.[0-9]+(\.[0-9]+)?$'
149 |
150 |
151 | ## ``unreleased_version_label`` is a string
152 | ##
153 | ## This label will be used as the changelog Title of the last set of changes
154 | ## between last valid tag and HEAD if any.
155 | unreleased_version_label = "%%version%% (unreleased)"
156 |
157 |
158 | ## ``output_engine`` is a callable
159 | ##
160 | ## This will change the output format of the generated changelog file
161 | ##
162 | ## Available choices are:
163 | ##
164 | ## - rest_py
165 | ##
166 | ## Legacy pure python engine, outputs ReSTructured text.
167 | ## This is the default.
168 | ##
169 | ## - mustache()
170 | ##
171 | ## Template name could be any of the available templates in
172 | ## ``templates/mustache/*.tpl``.
173 | ## Requires python package ``pystache``.
174 | ## Examples:
175 | ## - mustache("markdown")
176 | ## - mustache("restructuredtext")
177 | ##
178 | ## - makotemplate()
179 | ##
180 | ## Template name could be any of the available templates in
181 | ## ``templates/mako/*.tpl``.
182 | ## Requires python package ``mako``.
183 | ## Examples:
184 | ## - makotemplate("restructuredtext")
185 | ##
186 | output_engine = rest_py
187 | #output_engine = mustache("restructuredtext")
188 | #output_engine = mustache("markdown")
189 | #output_engine = makotemplate("restructuredtext")
190 |
191 |
192 | ## ``include_merges`` is a boolean
193 | ##
194 | ## This option tells git-log whether to include merge commits in the log.
195 | ## The default is to include them.
196 | include_merges = True
197 |
--------------------------------------------------------------------------------
/pdfutils/sites.py:
--------------------------------------------------------------------------------
1 | """
2 | Taken largely from django.contrib.admin
3 | """
4 | from functools import update_wrapper
5 | from django.http import Http404, HttpResponseRedirect
6 | from django.contrib.admin import ModelAdmin, actions
7 | from django.contrib.admin.forms import AdminAuthenticationForm
8 | from django.contrib.auth import logout as auth_logout, REDIRECT_FIELD_NAME
9 | from django.contrib.contenttypes import views as contenttype_views
10 | from django.views.decorators.csrf import csrf_protect
11 | from django.core.exceptions import ImproperlyConfigured
12 | from django.core.urlresolvers import reverse, NoReverseMatch
13 | from django.template.response import TemplateResponse
14 | from django.utils import six
15 | from django.utils.text import capfirst
16 | from django.utils.translation import ugettext as _
17 | from django.views.decorators.cache import never_cache
18 | from django.conf import settings
19 |
20 | from pdfutils.reports import ReportBase, Report
21 |
22 | LOGIN_FORM_KEY = 'this_is_the_login_form'
23 |
24 |
25 | class AlreadyRegistered(Exception):
26 | pass
27 |
28 |
29 | class NotRegistered(Exception):
30 | pass
31 |
32 |
33 | class ReportSite(object):
34 | """
35 | An AdminSite object encapsulates an instance of the Django admin application, ready
36 | to be hooked in to your URLconf. Models are registered with the AdminSite using the
37 | register() method, and the get_urls() method can then be used to access Django view
38 | functions that present a full admin interface for the collection of registered
39 | models.
40 | """
41 | index_template = None
42 |
43 | def __init__(self, name='pdfutils', app_name='report'):
44 | self._registry = {} # model_class class -> admin_class instance
45 | self.name = name
46 | self.app_name = app_name
47 |
48 | def register(self, report_or_iterable, **options):
49 | """
50 | Registers the given report(s) with the given admin class.
51 |
52 | The report(s) should be report classes, not instances.
53 |
54 | If a report is already registered, this will raise AlreadyRegistered.
55 |
56 | If a report is abstract, this will raise ImproperlyConfigured.
57 | """
58 | if issubclass(report_or_iterable, ReportBase):
59 | report_or_iterable = [report_or_iterable]
60 | for report in report_or_iterable:
61 | #if report._meta.abstract:
62 | # raise ImproperlyConfigured('The report %s is abstract, so it '
63 | # 'cannot be registered with pdfutils.' % report.__name__)
64 |
65 | if report in self._registry:
66 | raise AlreadyRegistered('The report %s is already registered' % report.__name__)
67 |
68 | #if admin_class is not Report and settings.DEBUG:
69 | # admin_class.validate(report)
70 |
71 | # Instantiate the admin class to save in the registry
72 | self._registry[report] = report(**options)
73 |
74 | def unregister(self, report_or_iterable):
75 | """
76 | Unregisters the given report(s).
77 |
78 | If a report isn't already registered, this will raise NotRegistered.
79 | """
80 | if isinstance(report_or_iterable, Report):
81 | report_or_iterable = [report_or_iterable]
82 | for report in report_or_iterable:
83 | if report not in self._registry:
84 | raise NotRegistered('The report %s is not registered' % report.__name__)
85 | del self._registry[report]
86 |
87 | def has_permission(self, request):
88 | """
89 | Returns True if the given HttpRequest has permission to view
90 | *at least one* page in the admin site.
91 | """
92 | return request.user.is_active and request.user.is_staff
93 |
94 | def check_dependencies(self):
95 | """
96 | Check that all things needed to run the admin have been correctly installed.
97 |
98 | The default implementation checks that LogEntry, ContentType and the
99 | auth context processor are installed.
100 | """
101 | from django.contrib.admin.models import LogEntry
102 | from django.contrib.contenttypes.models import ContentType
103 |
104 | if not LogEntry._meta.installed:
105 | raise ImproperlyConfigured("Put 'django.contrib.admin' in your "
106 | "INSTALLED_APPS setting in order to use the admin application.")
107 | if not ContentType._meta.installed:
108 | raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in "
109 | "your INSTALLED_APPS setting in order to use the admin application.")
110 | if not ('django.contrib.auth.context_processors.auth' in settings.TEMPLATE_CONTEXT_PROCESSORS or
111 | 'django.core.context_processors.auth' in settings.TEMPLATE_CONTEXT_PROCESSORS):
112 | raise ImproperlyConfigured("Put 'django.contrib.auth.context_processors.auth' "
113 | "in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.")
114 |
115 | def report_view(self, view, cacheable=False):
116 | """
117 | Decorator to create an admin view attached to this ``AdminSite``. This
118 | wraps the view and provides permission checking by calling
119 | ``self.has_permission``.
120 |
121 | You'll want to use this from within ``AdminSite.get_urls()``:
122 |
123 | class MyAdminSite(AdminSite):
124 |
125 | def get_urls(self):
126 | from django.conf.urls import patterns, url
127 |
128 | urls = super(MyAdminSite, self).get_urls()
129 | urls += patterns('',
130 | url(r'^my_view/$', self.report_view(some_view))
131 | )
132 | return urls
133 |
134 | By default, report_views are marked non-cacheable using the
135 | ``never_cache`` decorator. If the view can be safely cached, set
136 | cacheable=True.
137 | """
138 | def inner(request, *args, **kwargs):
139 | if LOGIN_FORM_KEY in request.POST and request.user.is_authenticated():
140 | auth_logout(request)
141 | if not self.has_permission(request):
142 | if request.path == reverse('admin:logout',
143 | current_app=self.name):
144 | index_path = reverse('admin:index', current_app=self.name)
145 | return HttpResponseRedirect(index_path)
146 | return self.login(request)
147 | return view(request, *args, **kwargs)
148 | if not cacheable:
149 | inner = never_cache(inner)
150 | # We add csrf_protect here so this function can be used as a utility
151 | # function for any view, without having to repeat 'csrf_protect'.
152 | if not getattr(view, 'csrf_exempt', False):
153 | inner = csrf_protect(inner)
154 | return update_wrapper(inner, view)
155 |
156 | @never_cache
157 | def report(self, request, extra_context=None):
158 | """
159 | Render report for the given HttpRequest.
160 |
161 | This should *not* assume the user is already logged in.
162 | """
163 | from django.contrib.auth.views import logout
164 | defaults = {
165 | 'current_app': self.name,
166 | 'extra_context': extra_context or {},
167 | }
168 | if self.report_template is not None:
169 | defaults['template_name'] = self.logout_template
170 | return logout(request, **defaults)
171 |
172 | def get_urls(self):
173 | from django.conf.urls import patterns, url, include
174 |
175 | if settings.DEBUG:
176 | self.check_dependencies()
177 |
178 | def wrap(view, cacheable=False):
179 | def wrapper(*args, **kwargs):
180 | return self.report_view(view, cacheable)(*args, **kwargs)
181 | return update_wrapper(wrapper, view)
182 |
183 | urlpatterns = patterns('')
184 |
185 | # Add in each report's views.
186 | for model_class, model_instance in six.iteritems(self._registry):
187 | urlpatterns += patterns('',
188 | url(r'^%s/' % model_instance.slug, model_class.as_view(),
189 | name=model_instance.slug))
190 | return urlpatterns
191 |
192 | @property
193 | def urls(self):
194 | return self.get_urls(), self.app_name, self.name
195 |
196 |
197 | # This global object represents the default report site, for the common case.
198 | # You can instantiate ReportSite in your own code to create a custom report site.
199 | site = ReportSite()
200 |
--------------------------------------------------------------------------------