Compare commits
1 Commits
main
...
wip/74797-
Author | SHA1 | Date |
---|---|---|
Thomas NOËL | 8d53545b72 |
|
@ -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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue