- `tags`: Similar to correspondent. Specify this multiple times to
have multiple tags added to the document.
- `archive_serial_number`: An optional archive serial number to set.
-- `custom_fields`: An array of custom field ids to assign (with an empty
- value) to the document.
+- `custom_fields`: Either an array of custom field ids to assign (with an empty
+ value) to the document or an object mapping field id -> value.
The endpoint will immediately return HTTP 200 if the document consumption
process was started successfully, with the UUID of the consumption task
max_value=Document.ARCHIVE_SERIAL_NUMBER_MAX,
)
- custom_fields = serializers.PrimaryKeyRelatedField(
- many=True,
- queryset=CustomField.objects.all(),
+ # Accept either a list of custom field ids or a dict mapping id -> value
+ custom_fields = serializers.JSONField(
label="Custom fields",
write_only=True,
required=False,
return None
def validate_custom_fields(self, custom_fields):
- if custom_fields:
- return [custom_field.id for custom_field in custom_fields]
- else:
+ if not custom_fields:
return None
+ # Normalize single values to a list
+ if isinstance(custom_fields, int):
+ custom_fields = [custom_fields]
+ if isinstance(custom_fields, dict):
+ custom_field_serializer = CustomFieldInstanceSerializer()
+ normalized = {}
+ for field_id, value in custom_fields.items():
+ try:
+ field_id_int = int(field_id)
+ except (TypeError, ValueError):
+ raise serializers.ValidationError(
+ _("Custom field id must be an integer: %(id)s")
+ % {"id": field_id},
+ )
+ try:
+ field = CustomField.objects.get(id=field_id_int)
+ except CustomField.DoesNotExist:
+ raise serializers.ValidationError(
+ _("Custom field with id %(id)s does not exist")
+ % {"id": field_id_int},
+ )
+ custom_field_serializer.validate(
+ {
+ "field": field,
+ "value": value,
+ },
+ )
+ normalized[field_id_int] = value
+ return normalized
+ elif isinstance(custom_fields, list):
+ try:
+ ids = [int(i) for i in custom_fields]
+ except (TypeError, ValueError):
+ raise serializers.ValidationError(
+ _(
+ "Custom fields must be a list of integers or an object mapping ids to values.",
+ ),
+ )
+ if CustomField.objects.filter(id__in=ids).count() != len(set(ids)):
+ raise serializers.ValidationError(
+ _("Some custom fields don't exist or were specified twice."),
+ )
+ return ids
+ raise serializers.ValidationError(
+ _(
+ "Custom fields must be a list of integers or an object mapping ids to values.",
+ ),
+ )
+
+ # custom_fields_w_values handled via validate_custom_fields
+
def validate_created(self, created):
# support datetime format for created for backwards compatibility
if isinstance(created, datetime):
import datetime
+import json
import shutil
import tempfile
import uuid
overrides.update(new_overrides)
self.assertEqual(overrides.custom_fields, {cf.id: None, cf2.id: 123})
+ def test_upload_with_custom_field_values(self):
+ """
+ GIVEN: A document with a source file
+ WHEN: Upload the document with custom fields and values
+ THEN: Metadata is set correctly
+ """
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ cf_string = CustomField.objects.create(
+ name="stringfield",
+ data_type=CustomField.FieldDataType.STRING,
+ )
+ cf_int = CustomField.objects.create(
+ name="intfield",
+ data_type=CustomField.FieldDataType.INT,
+ )
+
+ with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {
+ "document": f,
+ "custom_fields": json.dumps(
+ {
+ str(cf_string.id): "a string",
+ str(cf_int.id): 123,
+ },
+ ),
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ input_doc, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(input_doc.original_file.name, "simple.pdf")
+ self.assertEqual(overrides.filename, "simple.pdf")
+ self.assertEqual(
+ overrides.custom_fields,
+ {cf_string.id: "a string", cf_int.id: 123},
+ )
+
+ def test_upload_with_custom_fields_errors(self):
+ """
+ GIVEN: A document with a source file
+ WHEN: Upload the document with invalid custom fields payloads
+ THEN: The upload is rejected
+ """
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ error_payloads = [
+ # Non-integer key in mapping
+ {"custom_fields": json.dumps({"abc": "a string"})},
+ # List with non-integer entry
+ {"custom_fields": json.dumps(["abc"])},
+ # Nonexistent id in mapping
+ {"custom_fields": json.dumps({99999999: "a string"})},
+ # Nonexistent id in list
+ {"custom_fields": json.dumps([99999999])},
+ # Invalid type (JSON string, not list/dict/int)
+ {"custom_fields": json.dumps("not-a-supported-structure")},
+ ]
+
+ for payload in error_payloads:
+ with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
+ data = {"document": f, **payload}
+ response = self.client.post(
+ "/api/documents/post_document/",
+ data,
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ self.consume_file_mock.assert_not_called()
+
def test_upload_with_webui_source(self):
"""
GIVEN: A document with a source file
title = serializer.validated_data.get("title")
created = serializer.validated_data.get("created")
archive_serial_number = serializer.validated_data.get("archive_serial_number")
- custom_field_ids = serializer.validated_data.get("custom_fields")
+ cf = serializer.validated_data.get("custom_fields")
from_webui = serializer.validated_data.get("from_webui")
t = int(mktime(datetime.now().timetuple()))
source=DocumentSource.WebUI if from_webui else DocumentSource.ApiUpload,
original_file=temp_file_path,
)
+ custom_fields = None
+ if isinstance(cf, dict) and cf:
+ custom_fields = cf
+ elif isinstance(cf, list) and cf:
+ custom_fields = dict.fromkeys(cf, None)
input_doc_overrides = DocumentMetadataOverrides(
filename=doc_name,
title=title,
created=created,
asn=archive_serial_number,
owner_id=request.user.id,
- # TODO: set values
- custom_fields={cf_id: None for cf_id in custom_field_ids}
- if custom_field_ids
- else None,
+ custom_fields=custom_fields,
)
async_task = consume_file.delay(