diff --git a/passerelle/apps/pdf/forms.py b/passerelle/apps/pdf/forms.py
index 28e92e17..bf79723d 100644
--- a/passerelle/apps/pdf/forms.py
+++ b/passerelle/apps/pdf/forms.py
@@ -15,6 +15,8 @@
# along with this program. If not, see .
from django import forms
+from django.urls import reverse
+from django.utils.html import mark_safe
from django.utils.translation import gettext_lazy as _
from passerelle.utils.forms import ConditionField, TemplateField
@@ -44,11 +46,30 @@ class FieldsMappingEditForm(forms.ModelForm):
values = ', '.join('"%s"' % value for value in field.radio_possible_values)
help_text = _('text template, possibles values %s') % values
field_class = TemplateField
+ elif field.widget_type in ('list', 'combo'):
+ ds_url = (
+ reverse(
+ 'generic-endpoint',
+ kwargs={
+ 'connector': 'pdf',
+ 'endpoint': 'field-values',
+ 'slug': self.instance.slug,
+ },
+ )
+ + '?digest_id='
+ + field.digest_id
+ )
+ help_text = mark_safe(
+ _('text template, possibles values data source') % ds_url
+ )
+ field_class = TemplateField
else:
continue
- label = _('field {number} ({help_text})').format(number=i + 1, help_text=help_text)
+ label = _('field {number}').format(number=i + 1)
initial = fields_mapping.get(name, '')
- self.fields[name] = field_class(label=label, required=False, initial=initial)
+ self.fields[name] = field_class(
+ label=label, required=False, initial=initial, help_text=help_text
+ )
self.fields[name].page_number = page.page_number
self.fields[name].widget.attrs['tabindex'] = '0'
self.fields[name].widget.attrs['class'] = '0'
diff --git a/passerelle/apps/pdf/models.py b/passerelle/apps/pdf/models.py
index cd8bad56..76e652c2 100644
--- a/passerelle/apps/pdf/models.py
+++ b/passerelle/apps/pdf/models.py
@@ -133,6 +133,7 @@ class Resource(BaseResource):
description=_('Returns the assembly of received PDF files'),
perm='can_access',
methods=['post'],
+ display_order=0,
post={
'request_body': {'schema': {'application/json': ASSEMBLE_SCHEMA}},
'input_example': {
@@ -207,6 +208,7 @@ class Resource(BaseResource):
description=_('Fills the input PDF form with fields applying mappings to the received payload'),
perm='can_access',
methods=['post'],
+ display_order=1,
parameters={
'filename': {'description': _('file name')},
'flatten': {'description': _('remove PDF fields, keep only the drawed values')},
@@ -252,6 +254,8 @@ class Resource(BaseResource):
value = evaluate_template(mapping_template, post_data)
elif field.widget_type == 'radio':
value = evaluate_template(mapping_template, post_data)
+ elif field.widget_type in ('combo', 'list'):
+ value = evaluate_template(mapping_template, post_data)
self.logger.info('field=%r value=%r', field, value)
else:
raise NotImplementedError
@@ -261,3 +265,28 @@ class Resource(BaseResource):
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
pdf.write(response, flatten=flatten_pdf)
return response
+
+ @endpoint(
+ name='field-values',
+ description=_('Return possible values for PDF\'s combo or list form fields'),
+ perm='can_access',
+ parameters={
+ 'digest_id': {'description': _('Identifier of the field')},
+ },
+ )
+ def field_values(self, request, digest_id):
+ if not self.fill_form_file:
+ raise APIError('not PDF file configured')
+
+ with self.fill_form_file.open() as fd:
+ pdf_content = fd.read()
+
+ pdf = PDF(pdf_content)
+ fields = [field for page in pdf.pages for field in page.fields if field.digest_id == digest_id]
+ if not fields:
+ raise APIError(f'unknown digest-id {digest_id!r}')
+ field = fields[0]
+ if field.widget_type not in ('list', 'combo'):
+ raise APIError(f'wrong field type for digest-id {digest_id!r}: {field.widget_type}')
+
+ return {'data': [{'id': value, 'text': value} for _, value in field.combo_possible_values]}
diff --git a/passerelle/utils/pdf.py b/passerelle/utils/pdf.py
index 36edbd0e..240a036f 100644
--- a/passerelle/utils/pdf.py
+++ b/passerelle/utils/pdf.py
@@ -25,8 +25,9 @@ import typing
import pdfrw
-RADIO_FLAG = 2**15
-PUSH_BUTTON_FLAG = 2**16
+RADIO_FLAG = 1 << 15 # bit 16
+PUSH_BUTTON_FLAG = 1 << 16 # bit 17
+LIST_FLAG = 1 << 17 # bit 18
class Rect(typing.NamedTuple):
@@ -65,6 +66,10 @@ class FieldFlags(int):
def is_push_button(self):
return self & PUSH_BUTTON_FLAG
+ @property
+ def is_list(self):
+ return self & LIST_FLAG
+
@dataclasses.dataclass(frozen=True)
class Widget:
@@ -104,6 +109,11 @@ class Widget:
return 'checkbox'
elif self.field_type == pdfrw.PdfName.Tx:
return 'text'
+ elif self.field_type == pdfrw.PdfName.Ch:
+ if self.field_flags.is_list:
+ return 'list'
+ else:
+ return 'combo'
else:
raise NotImplementedError
@@ -156,6 +166,11 @@ class Widget:
assert self.widget_type == 'radio'
return list(list(kid.AP.N.keys())[0][1:] for kid in self.kids_ordered_by_rect if kid.AP and kid.AP.N)
+ @property
+ def combo_possible_values(self):
+ assert self.widget_type in ('list', 'combo')
+ return [(option[0].decode(), option[1].decode()) for option in self.annotation.Opt]
+
@property
def value(self):
if self.widget_type == 'text':
@@ -166,6 +181,8 @@ class Widget:
return self.annotation.V == self.checkbox_true_value
elif self.widget_type == 'radio':
return self.annotation.V.lstrip('/') if self.annotation.V else None
+ elif self.widget_type in ('list', 'combo'):
+ return self.annotation.V.decode() if self.annotation.V is not None else None
def set(self, value):
# allow rendering of values in Acrobat Reader
@@ -186,6 +203,15 @@ class Widget:
else:
kid.update(pdfrw.PdfDict(AS=pdfrw.PdfName.Off))
self.annotation.update(pdfrw.PdfDict(V=radio_value))
+ elif self.widget_type in ('list', 'combo'):
+ for export, combo_value in self.combo_possible_values:
+ if combo_value == value:
+ self.annotation.update(
+ pdfrw.PdfDict(
+ V=pdfrw.PdfString.from_unicode(export), AS=pdfrw.PdfString.from_unicode(export)
+ )
+ )
+ break
@classmethod
def from_pdf_widget(cls, page, pdf_widget):
diff --git a/tests/test_utils_pdf.py b/tests/test_utils_pdf.py
index 8289e9ef..dd13f9ad 100644
--- a/tests/test_utils_pdf.py
+++ b/tests/test_utils_pdf.py
@@ -104,3 +104,16 @@ def test_radio_button():
assert radio.radio_possible_values == ['H', 'F']
radio.set('H')
assert radio.value == 'H'
+
+
+def test_combo_box():
+ with open('tests/data/cerfa_14011-02.pdf', 'rb') as fd:
+ pdf = PDF(content=fd)
+ combo = [field for field in pdf.page(0).fields if field.name == 'topmostSubform[0].Page1[0].Pays[0]']
+ assert len(combo) == 1
+ combo = combo[0]
+ assert len(combo.combo_possible_values) == 235
+ combo.set('X')
+ assert combo.value is None
+ combo.set('FRANCE')
+ assert combo.value == 'FRANCE'