mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-16 21:55:37 -05:00
Compare commits
1 Commits
feature-po
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d225db4db1 |
@@ -192,8 +192,8 @@ The endpoint supports the following optional form fields:
|
||||
- `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`: Either an array of custom field ids to assign (with an empty
|
||||
value) to the document or an object mapping field id -> value.
|
||||
- `custom_fields`: An array of custom field ids to assign (with an empty
|
||||
value) to the document.
|
||||
|
||||
The endpoint will immediately return HTTP 200 if the document consumption
|
||||
process was started successfully, with the UUID of the consumption task
|
||||
|
@@ -1668,8 +1668,9 @@ class PostDocumentSerializer(serializers.Serializer):
|
||||
max_value=Document.ARCHIVE_SERIAL_NUMBER_MAX,
|
||||
)
|
||||
|
||||
# Accept either a list of custom field ids or a dict mapping id -> value
|
||||
custom_fields = serializers.JSONField(
|
||||
custom_fields = serializers.PrimaryKeyRelatedField(
|
||||
many=True,
|
||||
queryset=CustomField.objects.all(),
|
||||
label="Custom fields",
|
||||
write_only=True,
|
||||
required=False,
|
||||
@@ -1726,60 +1727,11 @@ class PostDocumentSerializer(serializers.Serializer):
|
||||
return None
|
||||
|
||||
def validate_custom_fields(self, custom_fields):
|
||||
if not custom_fields:
|
||||
if custom_fields:
|
||||
return [custom_field.id for custom_field in custom_fields]
|
||||
else:
|
||||
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):
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import datetime
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
@@ -1538,86 +1537,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
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
|
||||
|
@@ -1497,7 +1497,7 @@ class PostDocumentView(GenericAPIView):
|
||||
title = serializer.validated_data.get("title")
|
||||
created = serializer.validated_data.get("created")
|
||||
archive_serial_number = serializer.validated_data.get("archive_serial_number")
|
||||
cf = serializer.validated_data.get("custom_fields")
|
||||
custom_field_ids = serializer.validated_data.get("custom_fields")
|
||||
from_webui = serializer.validated_data.get("from_webui")
|
||||
|
||||
t = int(mktime(datetime.now().timetuple()))
|
||||
@@ -1516,11 +1516,6 @@ class PostDocumentView(GenericAPIView):
|
||||
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,
|
||||
@@ -1531,7 +1526,10 @@ class PostDocumentView(GenericAPIView):
|
||||
created=created,
|
||||
asn=archive_serial_number,
|
||||
owner_id=request.user.id,
|
||||
custom_fields=custom_fields,
|
||||
# TODO: set values
|
||||
custom_fields={cf_id: None for cf_id in custom_field_ids}
|
||||
if custom_field_ids
|
||||
else None,
|
||||
)
|
||||
|
||||
async_task = consume_file.delay(
|
||||
|
6
uv.lock
generated
6
uv.lock
generated
@@ -782,15 +782,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-guardian"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/ac/5a8f7301c0181ee9020e020a4fa519f9851726b8fd3c1177656c1f5a1be0/django_guardian-3.1.2.tar.gz", hash = "sha256:6fc93b55e5eacd1a062a959c5578b433d999a286742aa3e7e713c71046813538", size = 93422, upload-time = "2025-09-08T15:43:51.361Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/d3/436a44c7688fce1a978224c349ba66c95bf9103d548596b7a2694fd58c03/django_guardian-3.1.3.tar.gz", hash = "sha256:12b5e66c18c97088b0adfa033ab14be68c321c170fd3ec438898271f00a71699", size = 93571, upload-time = "2025-09-10T08:36:23.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/50/3a4a891f809c9865d30864f59f993f9535b18e3935dfad278c9682fc537f/django_guardian-3.1.2-py3-none-any.whl", hash = "sha256:6c10f88d0b7efd171ae65d7ac487a666f41eb373c6f94d80016b1a19bdfbf212", size = 127451, upload-time = "2025-09-08T15:43:49.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/fc/6fd7b8bc7c52cbbfd1714673cfd28ff0b3fae32265c52d492ec0dee22cb8/django_guardian-3.1.3-py3-none-any.whl", hash = "sha256:90e28b40eea65c326a3a961908cc300f9e1cd69b74e88d38317a9befa167b71c", size = 127687, upload-time = "2025-09-10T08:36:22.533Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
Reference in New Issue
Block a user