pdf: add xfdf template possibiity in fill-form endpoint (#74797)
gitea/passerelle/pipeline/head This commit looks good Details

This commit is contained in:
Thomas NOËL 2023-02-23 15:45:08 +01:00
parent eac46f952d
commit 8d53545b72
3 changed files with 96 additions and 40 deletions

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.26 on 2023-02-23 15:02
from django.db import migrations, models
import passerelle.utils.models
import passerelle.utils.templates
class Migration(migrations.Migration):
dependencies = [
('pdf', '0002_resource_fill_form_file'),
]
operations = [
migrations.AddField(
model_name='resource',
name='xfdf_template',
field=models.FileField(
blank=True,
help_text='Django template, used to create a XFDF for fill-form, rendered with payload',
null=True,
upload_to=passerelle.utils.models.resource_file_upload_to,
validators=[passerelle.utils.templates.validate_template],
verbose_name='XFDF Template',
),
),
]

View File

@ -25,12 +25,14 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.http.response import HttpResponse
from django.template.base import VariableDoesNotExist
from django.utils.translation import gettext_lazy as _
from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.models import resource_file_upload_to
from passerelle.utils.templates import render_to_string, validate_template
PDF_FILE_OBJECT = {
'type': 'object',
@ -85,7 +87,7 @@ FILL_FORM_SCHEMA = {
'title': '',
'description': '',
'type': 'object',
'required': ['filename', 'fields'],
'required': ['filename'],
'unflatten': True,
'properties': OrderedDict(
{
@ -94,7 +96,7 @@ FILL_FORM_SCHEMA = {
'type': 'string',
},
'input-form': PDF_FILE_OBJECT,
'fields': {
'xfdf': {
'description': _('hierarchical dictionary of fields'),
'type': 'object',
},
@ -123,6 +125,14 @@ class Resource(BaseResource):
null=True,
blank=True,
)
xfdf_template = models.FileField(
_('XFDF Template'),
upload_to=resource_file_upload_to,
help_text=_('Django template, used to create a XFDF for fill-form, rendered with payload'),
validators=[validate_template],
null=True,
blank=True,
)
class Meta:
verbose_name = _('PDF')
@ -191,34 +201,40 @@ class Resource(BaseResource):
'request_body': {'schema': {'application/json': FILL_FORM_SCHEMA}},
'input_example': {
'filename': 'filled.pdf',
'fields/Page1[0]/FirstName[0]': 'John',
'fields/Page1[0]/LastName[0]': 'Doe',
'fields/Page2[0]/Checkbox[0]': '0',
'fields/Page2[0]/Checkbox[1]': '1',
'xfdf/Page1[0]/FirstName[0]': 'John',
'xfdf/Page1[0]/LastName[0]': 'Doe',
'xfdf/Page2[0]/Checkbox[0]': '0',
'xfdf/Page2[0]/Checkbox[1]': '1',
},
},
)
def fill_form(self, request, post_data):
filename = post_data.pop('filename')
fields = post_data.pop('fields')
filename = post_data['filename']
if 'xfdf' in post_data:
fields = post_data.pop('xfdf')
elif self.xfdf_template:
fields = None
else:
raise APIError("missing 'xfdf' property (no XFDF template)", http_status=400)
xfdf_root = ET.Element('xfdf')
xfdf_root.attrib['xmlns'] = 'http://ns.adobe.com/xfdf/'
xfdf_root.attrib['xml:space'] = 'preserve'
xfdf_f = ET.SubElement(xfdf_root, 'f')
xfdf_fields = ET.SubElement(xfdf_root, 'fields')
if fields is not None:
xfdf_root = ET.Element('xfdf')
xfdf_root.attrib['xmlns'] = 'http://ns.adobe.com/xfdf/'
xfdf_root.attrib['xml:space'] = 'preserve'
xfdf_f = ET.SubElement(xfdf_root, 'f')
xfdf_fields = ET.SubElement(xfdf_root, 'fields')
def add_fields(element, fields):
if isinstance(fields, dict):
for key in fields:
field = ET.SubElement(element, 'field')
field.attrib['name'] = key
add_fields(field, fields[key])
else:
value = ET.SubElement(element, 'value')
value.text = str(fields)
def add_fields(element, fields):
if isinstance(fields, dict):
for key in fields:
field = ET.SubElement(element, 'field')
field.attrib['name'] = key
add_fields(field, fields[key])
else:
value = ET.SubElement(element, 'value')
value.text = str(fields)
add_fields(xfdf_fields, fields)
add_fields(xfdf_fields, fields)
with tempfile.TemporaryDirectory(prefix='passerelle-pdftk-%s-fill-form-' % self.id) as tmpdir:
if isinstance(post_data.get('input-form'), dict) and post_data['input-form'].get('content'):
@ -228,13 +244,25 @@ class Resource(BaseResource):
elif self.fill_form_file:
input_filename = self.fill_form_file.path
else:
raise APIError("missing or bad 'input-form' property", http_status=400)
raise APIError(
"missing or bad 'input-form' property (no default input file)", http_status=400
)
# create xfdf
xfdf_filename = os.path.join(tmpdir, 'fields.xfdf')
xfdf_f.attrib['href'] = input_filename
with open(xfdf_filename, mode='wb') as fd:
ET.indent(xfdf_root)
ET.ElementTree(xfdf_root).write(fd, encoding='UTF-8', xml_declaration=True)
if fields is not None:
xfdf_f.attrib['href'] = input_filename
with open(xfdf_filename, mode='wb') as fd:
ET.indent(xfdf_root)
ET.ElementTree(xfdf_root).write(fd, encoding='UTF-8', xml_declaration=True)
else:
self.xfdf_template.seek(0)
xfdf_template = self.xfdf_template.read().decode()
try:
xfdf_content = render_to_string(xfdf_template, post_data)
except VariableDoesNotExist as exc:
raise APIError("cannot render XFDF template: %s" % exc, http_status=400)
with open(xfdf_filename, mode='w') as fd:
fd.write(xfdf_content)
# call pdftk fill_form
pdf_content = self.run_pdftk(args=[input_filename, 'fill_form', xfdf_filename])
@ -254,5 +282,5 @@ class Resource(BaseResource):
for line in dump.splitlines():
unflatten_separated += '<br>%s' % line
if line.startswith('FieldName: '):
unflatten_separated += ' → <b>fields/%s</b>' % line[11:].replace('.', '/')
unflatten_separated += ' → <b>xfdf/%s</b>' % line[11:].replace('.', '/')
return unflatten_separated

View File

@ -154,7 +154,7 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
payload = {
'filename': 'foo.pdf',
'fields/fname': 'John',
'xfdf/fname': 'John',
'input-form': {'content': acroform_b64content},
}
mocked_check_output.side_effect = check_xml
@ -176,7 +176,7 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
pdf.save()
payload = {
'filename': 'bar.pdf',
'fields/fname': 'John',
'xfdf/fname': 'John',
}
mocked_check_output.reset_mock()
resp = app.post_json(endpoint, params=payload, status=200)
@ -196,7 +196,7 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
# pdftk errors (faked)
payload = {
'filename': 'foo.pdf',
'fields/fname': 'Bill',
'xfdf/fname': 'Bill',
'input-form': {'content': acroform_b64content},
}
mocked_check_output.reset_mock()
@ -228,22 +228,22 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
payload = {'filename': 'out.pdf'}
resp = app.post_json(endpoint, params=payload, status=400)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == "'fields' is a required property"
assert resp.json['err_desc'] == "missing 'xfdf' property (no XFDF template)"
payload = {'filename': 'out.pdf', 'fields': 'not-a-dict'}
payload = {'filename': 'out.pdf', 'xfdf': 'not-a-dict'}
resp = app.post_json(endpoint, params=payload, status=400)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == "fields: 'not-a-dict' is not of type 'object'"
assert resp.json['err_desc'] == "xfdf: 'not-a-dict' is not of type 'object'"
pdf.fill_form_file = None # no default PDF form
pdf.save()
payload = {
'filename': 'bar.pdf',
'fields/fname': 'Alice',
'xfdf/fname': 'Alice',
}
resp = app.post_json(endpoint, params=payload, status=400)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == "missing or bad 'input-form' property"
assert resp.json['err_desc'] == "missing or bad 'input-form' property (no default input file)"
resp = app.get(endpoint, status=405)
@ -255,7 +255,7 @@ def test_pdf_real_pdftk_fillform(admin_user, app, pdf, settings):
endpoint = generic_endpoint_url('pdf', 'fill-form', slug=pdf.slug)
payload = {
'filename': 'filled.pdf',
'fields/fname': 'ThisIsMyFirstName',
'xfdf/fname': 'ThisIsMyFirstName',
'input-form': {'content': acroform_b64content},
}
resp = app.post_json(endpoint, params=payload, status=200)
@ -271,11 +271,11 @@ def test_pdf_real_pdftk_fillform(admin_user, app, pdf, settings):
manage_url = reverse('view-connector', kwargs={'connector': 'pdf', 'slug': pdf.slug})
resp = app.get(manage_url)
assert 'panel-dumpfields' not in resp.text
assert '<b>fields/fname</b>' not in resp.text
assert '<b>xfdf/fname</b>' not in resp.text
app = login(app)
resp = app.get(manage_url)
assert 'panel-dumpfields' in resp.text
assert '<b>fields/fname</b>' in resp.text
assert '<b>xfdf/fname</b>' in resp.text
def test_pdf_validator(pdf):