Compare commits

...

1 Commits

Author SHA1 Message Date
Thomas NOËL 8d53545b72 pdf: add xfdf template possibiity in fill-form endpoint (#74797)
gitea/passerelle/pipeline/head This commit looks good Details
2023-02-23 18:12:23 +01:00
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.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.template.base import VariableDoesNotExist
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passerelle.base.models import BaseResource from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError from passerelle.utils.jsonresponse import APIError
from passerelle.utils.models import resource_file_upload_to from passerelle.utils.models import resource_file_upload_to
from passerelle.utils.templates import render_to_string, validate_template
PDF_FILE_OBJECT = { PDF_FILE_OBJECT = {
'type': 'object', 'type': 'object',
@ -85,7 +87,7 @@ FILL_FORM_SCHEMA = {
'title': '', 'title': '',
'description': '', 'description': '',
'type': 'object', 'type': 'object',
'required': ['filename', 'fields'], 'required': ['filename'],
'unflatten': True, 'unflatten': True,
'properties': OrderedDict( 'properties': OrderedDict(
{ {
@ -94,7 +96,7 @@ FILL_FORM_SCHEMA = {
'type': 'string', 'type': 'string',
}, },
'input-form': PDF_FILE_OBJECT, 'input-form': PDF_FILE_OBJECT,
'fields': { 'xfdf': {
'description': _('hierarchical dictionary of fields'), 'description': _('hierarchical dictionary of fields'),
'type': 'object', 'type': 'object',
}, },
@ -123,6 +125,14 @@ class Resource(BaseResource):
null=True, null=True,
blank=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: class Meta:
verbose_name = _('PDF') verbose_name = _('PDF')
@ -191,34 +201,40 @@ class Resource(BaseResource):
'request_body': {'schema': {'application/json': FILL_FORM_SCHEMA}}, 'request_body': {'schema': {'application/json': FILL_FORM_SCHEMA}},
'input_example': { 'input_example': {
'filename': 'filled.pdf', 'filename': 'filled.pdf',
'fields/Page1[0]/FirstName[0]': 'John', 'xfdf/Page1[0]/FirstName[0]': 'John',
'fields/Page1[0]/LastName[0]': 'Doe', 'xfdf/Page1[0]/LastName[0]': 'Doe',
'fields/Page2[0]/Checkbox[0]': '0', 'xfdf/Page2[0]/Checkbox[0]': '0',
'fields/Page2[0]/Checkbox[1]': '1', 'xfdf/Page2[0]/Checkbox[1]': '1',
}, },
}, },
) )
def fill_form(self, request, post_data): def fill_form(self, request, post_data):
filename = post_data.pop('filename') filename = post_data['filename']
fields = post_data.pop('fields') 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') if fields is not None:
xfdf_root.attrib['xmlns'] = 'http://ns.adobe.com/xfdf/' xfdf_root = ET.Element('xfdf')
xfdf_root.attrib['xml:space'] = 'preserve' xfdf_root.attrib['xmlns'] = 'http://ns.adobe.com/xfdf/'
xfdf_f = ET.SubElement(xfdf_root, 'f') xfdf_root.attrib['xml:space'] = 'preserve'
xfdf_fields = ET.SubElement(xfdf_root, 'fields') xfdf_f = ET.SubElement(xfdf_root, 'f')
xfdf_fields = ET.SubElement(xfdf_root, 'fields')
def add_fields(element, fields): def add_fields(element, fields):
if isinstance(fields, dict): if isinstance(fields, dict):
for key in fields: for key in fields:
field = ET.SubElement(element, 'field') field = ET.SubElement(element, 'field')
field.attrib['name'] = key field.attrib['name'] = key
add_fields(field, fields[key]) add_fields(field, fields[key])
else: else:
value = ET.SubElement(element, 'value') value = ET.SubElement(element, 'value')
value.text = str(fields) 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: 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'): 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: elif self.fill_form_file:
input_filename = self.fill_form_file.path input_filename = self.fill_form_file.path
else: 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 # create xfdf
xfdf_filename = os.path.join(tmpdir, 'fields.xfdf') xfdf_filename = os.path.join(tmpdir, 'fields.xfdf')
xfdf_f.attrib['href'] = input_filename if fields is not None:
with open(xfdf_filename, mode='wb') as fd: xfdf_f.attrib['href'] = input_filename
ET.indent(xfdf_root) with open(xfdf_filename, mode='wb') as fd:
ET.ElementTree(xfdf_root).write(fd, encoding='UTF-8', xml_declaration=True) 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 # call pdftk fill_form
pdf_content = self.run_pdftk(args=[input_filename, 'fill_form', xfdf_filename]) pdf_content = self.run_pdftk(args=[input_filename, 'fill_form', xfdf_filename])
@ -254,5 +282,5 @@ class Resource(BaseResource):
for line in dump.splitlines(): for line in dump.splitlines():
unflatten_separated += '<br>%s' % line unflatten_separated += '<br>%s' % line
if line.startswith('FieldName: '): 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 return unflatten_separated

View File

@ -154,7 +154,7 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
payload = { payload = {
'filename': 'foo.pdf', 'filename': 'foo.pdf',
'fields/fname': 'John', 'xfdf/fname': 'John',
'input-form': {'content': acroform_b64content}, 'input-form': {'content': acroform_b64content},
} }
mocked_check_output.side_effect = check_xml mocked_check_output.side_effect = check_xml
@ -176,7 +176,7 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
pdf.save() pdf.save()
payload = { payload = {
'filename': 'bar.pdf', 'filename': 'bar.pdf',
'fields/fname': 'John', 'xfdf/fname': 'John',
} }
mocked_check_output.reset_mock() mocked_check_output.reset_mock()
resp = app.post_json(endpoint, params=payload, status=200) 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) # pdftk errors (faked)
payload = { payload = {
'filename': 'foo.pdf', 'filename': 'foo.pdf',
'fields/fname': 'Bill', 'xfdf/fname': 'Bill',
'input-form': {'content': acroform_b64content}, 'input-form': {'content': acroform_b64content},
} }
mocked_check_output.reset_mock() mocked_check_output.reset_mock()
@ -228,22 +228,22 @@ def test_pdf_fill_form(mocked_check_output, app, pdf):
payload = {'filename': 'out.pdf'} payload = {'filename': 'out.pdf'}
resp = app.post_json(endpoint, params=payload, status=400) resp = app.post_json(endpoint, params=payload, status=400)
assert resp.json['err'] == 1 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) resp = app.post_json(endpoint, params=payload, status=400)
assert resp.json['err'] == 1 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.fill_form_file = None # no default PDF form
pdf.save() pdf.save()
payload = { payload = {
'filename': 'bar.pdf', 'filename': 'bar.pdf',
'fields/fname': 'Alice', 'xfdf/fname': 'Alice',
} }
resp = app.post_json(endpoint, params=payload, status=400) resp = app.post_json(endpoint, params=payload, status=400)
assert resp.json['err'] == 1 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) 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) endpoint = generic_endpoint_url('pdf', 'fill-form', slug=pdf.slug)
payload = { payload = {
'filename': 'filled.pdf', 'filename': 'filled.pdf',
'fields/fname': 'ThisIsMyFirstName', 'xfdf/fname': 'ThisIsMyFirstName',
'input-form': {'content': acroform_b64content}, 'input-form': {'content': acroform_b64content},
} }
resp = app.post_json(endpoint, params=payload, status=200) 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}) manage_url = reverse('view-connector', kwargs={'connector': 'pdf', 'slug': pdf.slug})
resp = app.get(manage_url) resp = app.get(manage_url)
assert 'panel-dumpfields' not in resp.text 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) app = login(app)
resp = app.get(manage_url) resp = app.get(manage_url)
assert 'panel-dumpfields' in resp.text 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): def test_pdf_validator(pdf):