Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Odt engine #2

Merged
merged 2 commits into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,9 @@ venv.bak/

# mypy
.mypy_cache/

# linting
.flake8

# vscode
.vscode
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dist: xenial
language: python
python:
- "3.6"
install:
- pip install tox
script: tox
Empty file added CHANGES.md
Empty file.
3 changes: 3 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage
pylama
isort
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Django==2.2.*
requests==2.22.*
Pillow==6.1.*
python-magic==0.4.*
15 changes: 15 additions & 0 deletions runtests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
from os import environ
from sys import exit as sys_exit

from django import setup
from django.conf import settings
from django.test.utils import get_runner

if __name__ == '__main__':
environ['DJANGO_SETTINGS_MODULE'] = 'test_template_engines.settings'
setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(['test_template_engines.tests'])
sys_exit(bool(failures))
37 changes: 37 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python
from os.path import dirname, join, realpath

from setuptools import setup

HERE = dirname(realpath(__file__))

README = open(join(HERE, 'README.md')).read()
CHANGES = open(join(HERE, 'CHANGES.md')).read()
PACKAGES = ['template_engines']
PACKAGE_DIR = {'template_engines': 'template_engines'}


setup(
name='template_engines',
version='0.0.1',
author_email='python@makina-corpus.org',
description='Additional template engines for Django.',
long_description=README + '\n\n' + CHANGES,
packages=PACKAGES,
package_dir=PACKAGE_DIR,
include_package_data=True,
url='https://github.com/Terralego/django-template-engines',
classifiers=[
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.6',
],
install_requires=['Django>=2.2.3', 'Pillow>=6.1.0', 'requests>=2.22.0', 'python-magic>=0.4.15'],
)
Empty file added template_engines/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions template_engines/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class TemplateEnginesConfig(AppConfig):
name = 'template_engines'
Empty file.
62 changes: 62 additions & 0 deletions template_engines/backends/abstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from glob import glob
from os.path import isdir, isfile, join

from django.template import Template
from django.template.backends.base import BaseEngine
from django.template.loader import TemplateDoesNotExist
from django.utils.functional import cached_property


class AbstractEngine(BaseEngine):
"""
Gives the architecture of a basic template engine and two methods implemented:
``get_template_path`` and ``from_string``.

Can be specified:
* ``app_dirname``, the folder name which contains the templates in application directories,
* ``sub_dirname``, the folder name of the subdirectory in the templates directory,
* ``template_class``, your own template class with a ``render`` method.

``get_template`` must be implemented.
"""
app_dirname = None
sub_dirname = None
template_class = None

def __init__(self, params):
params = params.copy()
self.options = params.pop('OPTIONS')
super().__init__(params)

@cached_property
def template_dirs(self):
t_dirs = super().template_dirs
if self.sub_dirname:
t_dirs += tuple([join(p, self.sub_dirname)
for p in t_dirs
if isdir(join(p, self.sub_dirname))])
return t_dirs

def from_string(self, template_code, **kwargs):
return self.template_class(Template(template_code), **kwargs)

def get_template_path(self, template_name):
"""
Check if a template named ``template_name`` can be found in a list of directories. Returns
the path if the file exists or raises ``TemplateDoesNotExist`` otherwise.
"""
if isfile(template_name):
return template_name
template_path = None
for directory in self.template_dirs:
abstract_path = join(directory, template_name)
path = glob(abstract_path)
if path:
template_path = path[0]
break
if template_path is None:
raise TemplateDoesNotExist(f'Unknown: {template_name}')
return template_path

def get_template(self, template_name):
raise NotImplementedError()
76 changes: 76 additions & 0 deletions template_engines/backends/odt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from re import findall, sub
from zipfile import ZipFile

from django.conf import settings
from django.template import Context
from django.template.context import make_context

from .abstract import AbstractEngine
from .utils import modify_zip_file


def odt_handler(read_zip_file, write_zip_file, item, rendered):
if item.filename != 'content.xml':
write_zip_file.writestr(item, read_zip_file.read(item.filename))
else:
write_zip_file.writestr(item, rendered)


class OdtTemplate:
"""
Handles odt templates.
"""

def __init__(self, template, **kwargs):
"""
:param template: the template to fill.
:type template: django.template.Template

:param kwargs: it must contain a `template_path`.
"""
self.template = template
self.template_path = kwargs.pop('template_path')

def render(self, context=None, request=None):
"""
Fills an odt template with the context obtained by combining the `context` and` request` \
parameters and returns an odt file as a byte object.
"""
context = make_context(context, request)
rendered = self.template.render(Context(context))
while len(findall('\n', rendered)) > 1:
rendered = sub(
'<text:p([^>]+)>([^<\n]*)\n',
'<text:p\\g<1>>\\g<2></text:p><text:p\\g<1>>',
rendered,
)
odt_content = modify_zip_file(self.template_path, odt_handler, rendered)
return odt_content


class OdtEngine(AbstractEngine):
"""
Odt template engine.

By default, ``app_dirname`` is equal to 'templates' but you can change this value by adding an
``ODT_ENGINE_APP_DIRNAME`` setting in your settings.
By default, ``sub_dirname`` is equal to 'odt' but you can change this value by adding an
``ODT_ENGINE_SUB_DIRNAME`` setting in your settings.
By default, ``OdtTemplate`` is used as template_class.
"""
sub_dirname = getattr(settings, 'ODT_ENGINE_SUB_DIRNAME', 'odt')
app_dirname = getattr(settings, 'ODT_ENGINE_APP_DIRNAME', 'templates')
template_class = OdtTemplate

def get_template_content(self, template_path):
"""
Returns the contents of a template before modification, as a string.
"""
with ZipFile(template_path, 'r') as zip_file:
b_content = zip_file.read('content.xml')
return b_content.decode()

def get_template(self, template_name):
template_path = self.get_template_path(template_name)
content = self.get_template_content(template_path)
return self.from_string(content, template_path=template_path)
36 changes: 36 additions & 0 deletions template_engines/backends/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Contains all generic functions that can be used to build backends.
"""
from tempfile import NamedTemporaryFile
from zipfile import ZipFile


def modify_zip_file(file_path, handler, rendered):
"""
Modify a zip file.
:param file_path: the path of the zip file.
:type file_path: str

:param handler: you must write a function that takes, the result of ZipFile(..., 'r') as \
read_zip_file ; the result of ZipFile(..., 'w') as write_zip_file ; item an element of \
read_zip_file.infolist() and kwargs. This function will modify the file at your convinience.
:type handler: function

:param kwargs: some things that your handler may need.

:returns: your modified zip file as a byte object.

.. note :: this handler makes a copy
::

def copy_handler(read_zip_file, write_zip_file, item, **kwargs):
write_zip_file.writestr(item, read_zip_file.read(item.filename))
"""
temp_file = NamedTemporaryFile()
with ZipFile(file_path, 'r') as read_zip_file:
info_list = read_zip_file.infolist()
with ZipFile(temp_file.name, 'w') as write_zip_file:
for item in info_list:
handler(read_zip_file, write_zip_file, item, rendered)
with open(temp_file.name, 'rb') as read_file:
return read_file.read()
Empty file.
50 changes: 50 additions & 0 deletions template_engines/templatetags/libreoffice_image_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from base64 import b64encode
from io import BytesIO

from django import template
from django.utils.safestring import mark_safe
from magic import from_buffer
from PIL import Image

from .utils import resize

register = template.Library()
IMAGE_MIME_TYPE = ('image/jpeg', 'image/png', 'image/svg+xml')


@register.simple_tag
def libreoffice_image_loader(image):
"""
Replace a tag by an image you specified.
You must add an entry to the `context` var that is a dict with at least a `content` key whose
value is a byte object. You can also specify `width` and `height`, otherwise it will
automatically resize your image.
"""
if not isinstance(image, dict):
return None

width = image.get('width')
height = image.get('height')
content = image.get('content')

if not isinstance(content, bytes):
return None

mime_type = from_buffer(content, mime=True)
if mime_type not in IMAGE_MIME_TYPE:
return None

if not width and not height:
buffer = BytesIO(content)
with Image.open(buffer) as img_reader:
width, height = resize(*img_reader.size)

return mark_safe(
f'<draw:frame draw:name="img1" svg:width="{width}" svg:height="{height}">'
+ '<draw:image xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad">'
+ '<office:binary-data>'
+ b64encode(content).decode()
+ '</office:binary-data>'
+ '</draw:image>'
+ '</draw:frame>'
)
3 changes: 3 additions & 0 deletions template_engines/templatetags/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def resize(width, height):
ratio = min(16697 / width, 28815 / height)
return width * ratio, height * ratio
Binary file added templates/odt/template.odt
Binary file not shown.
Empty file.
5 changes: 5 additions & 0 deletions test_template_engines/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class TestTemplateEnginesConfig(AppConfig):
name = 'test_template_engines'
Binary file added test_template_engines/makina-corpus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions test_template_engines/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 2.2.3 on 2019-07-09 09:38

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Bidon',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
),
]
Empty file.
5 changes: 5 additions & 0 deletions test_template_engines/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.db import models


class Bidon(models.Model):
name = models.CharField(max_length=255)
Loading