Compare commits

..

12 Commits

Author SHA1 Message Date
shamoon
e6da2a94d1 coverage, unify error tests 2025-09-15 01:23:04 -07:00
shamoon
8d0581177e Update serialisers.py 2025-09-15 00:19:48 -07:00
shamoon
3ef0b89e6c typo 2025-09-15 00:01:19 -07:00
shamoon
42463d68a0 simplify 2025-09-14 23:56:36 -07:00
shamoon
4c2e361762 Update views.py 2025-09-14 23:54:35 -07:00
shamoon
10c254e96d Maybe handle this backwards compat 2025-09-14 22:57:04 -07:00
shamoon
90b2f694c0 Update api.md 2025-09-14 22:22:53 -07:00
shamoon
c02907ff37 Update serialisers.py 2025-09-14 22:21:54 -07:00
shamoon
a2d89e7633 Actually lets unify it 2025-09-14 22:19:36 -07:00
shamoon
1d6cdf7b1d Use json str, normalize keys 2025-09-14 21:37:37 -07:00
shamoon
5a8b470673 Add negative test 2025-09-14 19:47:57 -07:00
shamoon
b2f1c5a6af Enhancement: support custom field values with post document endpoint 2025-09-14 19:38:52 -07:00
9 changed files with 167 additions and 36 deletions

View File

@@ -192,8 +192,8 @@ The endpoint supports the following optional form fields:
- `tags`: Similar to correspondent. Specify this multiple times to - `tags`: Similar to correspondent. Specify this multiple times to
have multiple tags added to the document. have multiple tags added to the document.
- `archive_serial_number`: An optional archive serial number to set. - `archive_serial_number`: An optional archive serial number to set.
- `custom_fields`: An array of custom field ids to assign (with an empty - `custom_fields`: Either an array of custom field ids to assign (with an empty
value) to the document. value) to the document or an object mapping field id -> value.
The endpoint will immediately return HTTP 200 if the document consumption The endpoint will immediately return HTTP 200 if the document consumption
process was started successfully, with the UUID of the consumption task process was started successfully, with the UUID of the consumption task

View File

@@ -5192,7 +5192,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
<target state="translated">Té ruta emmagatzematge</target> <target state="needs-translation">Has storage path</target>
</trans-unit> </trans-unit>
<trans-unit id="6417103744331194518" datatype="html"> <trans-unit id="6417103744331194518" datatype="html">
<source>Action type</source> <source>Action type</source>
@@ -7264,7 +7264,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">58</context>
</context-group> </context-group>
<target state="translated">Imprimir</target> <target state="needs-translation">Print</target>
</trans-unit> </trans-unit>
<trans-unit id="1418444397960583910" datatype="html"> <trans-unit id="1418444397960583910" datatype="html">
<source>More like this</source> <source>More like this</source>
@@ -7852,7 +7852,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1452</context> <context context-type="linenumber">1452</context>
</context-group> </context-group>
<target state="translated">Impressió fallida.</target> <target state="needs-translation">Print failed.</target>
</trans-unit> </trans-unit>
<trans-unit id="6457245677384603573" datatype="html"> <trans-unit id="6457245677384603573" datatype="html">
<source>Error loading document for printing.</source> <source>Error loading document for printing.</source>
@@ -7860,7 +7860,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1460</context> <context context-type="linenumber">1460</context>
</context-group> </context-group>
<target state="translated">Error carregant document per imprimir.</target> <target state="needs-translation">Error loading document for printing.</target>
</trans-unit> </trans-unit>
<trans-unit id="6085793215710522488" datatype="html"> <trans-unit id="6085793215710522488" datatype="html">
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
@@ -10180,7 +10180,7 @@
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">55</context>
</context-group> </context-group>
<target state="translated">Text Llarg</target> <target state="needs-translation">Long Text</target>
</trans-unit> </trans-unit>
<trans-unit id="4460262093225954455" datatype="html"> <trans-unit id="4460262093225954455" datatype="html">
<source>Search score</source> <source>Search score</source>

View File

@@ -5192,7 +5192,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
<target state="translated">Ima putanju za skladištenje</target> <target state="needs-translation">Has storage path</target>
</trans-unit> </trans-unit>
<trans-unit id="6417103744331194518" datatype="html"> <trans-unit id="6417103744331194518" datatype="html">
<source>Action type</source> <source>Action type</source>
@@ -6874,7 +6874,7 @@
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">257</context> <context context-type="linenumber">257</context>
</context-group> </context-group>
<target state="translated">WebSocket konekcija</target> <target state="needs-translation">WebSocket Connection</target>
</trans-unit> </trans-unit>
<trans-unit id="8998179362936748717" datatype="html"> <trans-unit id="8998179362936748717" datatype="html">
<source>OK</source> <source>OK</source>
@@ -6882,7 +6882,7 @@
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">261</context> <context context-type="linenumber">261</context>
</context-group> </context-group>
<target state="translated">OK</target> <target state="needs-translation">OK</target>
</trans-unit> </trans-unit>
<trans-unit id="6732151329960766506" datatype="html"> <trans-unit id="6732151329960766506" datatype="html">
<source>Copy Raw Error</source> <source>Copy Raw Error</source>
@@ -7264,7 +7264,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">58</context>
</context-group> </context-group>
<target state="translated">Štampaj</target> <target state="needs-translation">Print</target>
</trans-unit> </trans-unit>
<trans-unit id="1418444397960583910" datatype="html"> <trans-unit id="1418444397960583910" datatype="html">
<source>More like this</source> <source>More like this</source>
@@ -7852,7 +7852,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1452</context> <context context-type="linenumber">1452</context>
</context-group> </context-group>
<target state="translated">Štampanje nije uspelo.</target> <target state="needs-translation">Print failed.</target>
</trans-unit> </trans-unit>
<trans-unit id="6457245677384603573" datatype="html"> <trans-unit id="6457245677384603573" datatype="html">
<source>Error loading document for printing.</source> <source>Error loading document for printing.</source>
@@ -7860,7 +7860,7 @@
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1460</context> <context context-type="linenumber">1460</context>
</context-group> </context-group>
<target state="translated">Greška pri učitavanju dokumenta za štampanje.</target> <target state="needs-translation">Error loading document for printing.</target>
</trans-unit> </trans-unit>
<trans-unit id="6085793215710522488" datatype="html"> <trans-unit id="6085793215710522488" datatype="html">
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
@@ -10181,7 +10181,7 @@
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">55</context>
</context-group> </context-group>
<target state="translated">Dugi tekst</target> <target state="needs-translation">Long Text</target>
</trans-unit> </trans-unit>
<trans-unit id="4460262093225954455" datatype="html"> <trans-unit id="4460262093225954455" datatype="html">
<source>Search score</source> <source>Search score</source>

View File

@@ -1668,9 +1668,8 @@ class PostDocumentSerializer(serializers.Serializer):
max_value=Document.ARCHIVE_SERIAL_NUMBER_MAX, max_value=Document.ARCHIVE_SERIAL_NUMBER_MAX,
) )
custom_fields = serializers.PrimaryKeyRelatedField( # Accept either a list of custom field ids or a dict mapping id -> value
many=True, custom_fields = serializers.JSONField(
queryset=CustomField.objects.all(),
label="Custom fields", label="Custom fields",
write_only=True, write_only=True,
required=False, required=False,
@@ -1727,11 +1726,60 @@ class PostDocumentSerializer(serializers.Serializer):
return None return None
def validate_custom_fields(self, custom_fields): def validate_custom_fields(self, custom_fields):
if custom_fields: if not custom_fields:
return [custom_field.id for custom_field in custom_fields]
else:
return None 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): def validate_created(self, created):
# support datetime format for created for backwards compatibility # support datetime format for created for backwards compatibility
if isinstance(created, datetime): if isinstance(created, datetime):

View File

@@ -1,4 +1,5 @@
import datetime import datetime
import json
import shutil import shutil
import tempfile import tempfile
import uuid import uuid
@@ -1537,6 +1538,86 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
overrides.update(new_overrides) overrides.update(new_overrides)
self.assertEqual(overrides.custom_fields, {cf.id: None, cf2.id: 123}) 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): def test_upload_with_webui_source(self):
""" """
GIVEN: A document with a source file GIVEN: A document with a source file

View File

@@ -1497,7 +1497,7 @@ class PostDocumentView(GenericAPIView):
title = serializer.validated_data.get("title") title = serializer.validated_data.get("title")
created = serializer.validated_data.get("created") created = serializer.validated_data.get("created")
archive_serial_number = serializer.validated_data.get("archive_serial_number") 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") from_webui = serializer.validated_data.get("from_webui")
t = int(mktime(datetime.now().timetuple())) t = int(mktime(datetime.now().timetuple()))
@@ -1516,6 +1516,11 @@ class PostDocumentView(GenericAPIView):
source=DocumentSource.WebUI if from_webui else DocumentSource.ApiUpload, source=DocumentSource.WebUI if from_webui else DocumentSource.ApiUpload,
original_file=temp_file_path, 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( input_doc_overrides = DocumentMetadataOverrides(
filename=doc_name, filename=doc_name,
title=title, title=title,
@@ -1526,10 +1531,7 @@ class PostDocumentView(GenericAPIView):
created=created, created=created,
asn=archive_serial_number, asn=archive_serial_number,
owner_id=request.user.id, owner_id=request.user.id,
# TODO: set values custom_fields=custom_fields,
custom_fields={cf_id: None for cf_id in custom_field_ids}
if custom_field_ids
else None,
) )
async_task = consume_file.delay( async_task = consume_file.delay(

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-14 03:21+0000\n" "POT-Creation-Date: 2025-09-14 03:21+0000\n"
"PO-Revision-Date: 2025-09-17 00:33\n" "PO-Revision-Date: 2025-09-14 03:22\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
@@ -19,7 +19,7 @@ msgstr ""
#: documents/apps.py:8 #: documents/apps.py:8
msgid "Documents" msgid "Documents"
msgstr "Documents " msgstr "documents"
#: documents/filters.py:386 #: documents/filters.py:386
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
@@ -750,7 +750,7 @@ msgstr "Selecciona"
#: documents/models.py:762 #: documents/models.py:762
msgid "Long Text" msgid "Long Text"
msgstr "Text Llarg" msgstr ""
#: documents/models.py:774 #: documents/models.py:774
msgid "data type" msgid "data type"
@@ -858,7 +858,7 @@ msgstr "té aquest corresponsal"
#: documents/models.py:1056 #: documents/models.py:1056
msgid "has this storage path" msgid "has this storage path"
msgstr "té aquesta ruta emmagatzematge" msgstr ""
#: documents/models.py:1060 #: documents/models.py:1060
msgid "schedule offset days" msgid "schedule offset days"
@@ -1002,7 +1002,7 @@ msgstr "assigna títol"
#: documents/models.py:1227 #: documents/models.py:1227
msgid "Assign a document title, must be a Jinja2 template, see documentation." msgid "Assign a document title, must be a Jinja2 template, see documentation."
msgstr "Asigna títol de doculent, ha de ser plantilla Jinja2, veure documentació." msgstr ""
#: documents/models.py:1235 paperless_mail/models.py:274 #: documents/models.py:1235 paperless_mail/models.py:274
msgid "assign this tag" msgid "assign this tag"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-14 03:21+0000\n" "POT-Creation-Date: 2025-09-14 03:21+0000\n"
"PO-Revision-Date: 2025-09-17 00:33\n" "PO-Revision-Date: 2025-09-14 03:22\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Serbian (Latin)\n" "Language-Team: Serbian (Latin)\n"
"Language: sr_CS\n" "Language: sr_CS\n"
@@ -750,7 +750,7 @@ msgstr "Odaberi"
#: documents/models.py:762 #: documents/models.py:762
msgid "Long Text" msgid "Long Text"
msgstr "Dugi tekst" msgstr ""
#: documents/models.py:774 #: documents/models.py:774
msgid "data type" msgid "data type"
@@ -858,7 +858,7 @@ msgstr "poseduje ovog korespondenta"
#: documents/models.py:1056 #: documents/models.py:1056
msgid "has this storage path" msgid "has this storage path"
msgstr "ima ovu putanju za skladištenje" msgstr ""
#: documents/models.py:1060 #: documents/models.py:1060
msgid "schedule offset days" msgid "schedule offset days"
@@ -1002,7 +1002,7 @@ msgstr "dodeli naslov"
#: documents/models.py:1227 #: documents/models.py:1227
msgid "Assign a document title, must be a Jinja2 template, see documentation." msgid "Assign a document title, must be a Jinja2 template, see documentation."
msgstr "Dodelite naslov dokumenta, mora biti Jinja2 šablon, pogledajte dokumentaciju." msgstr ""
#: documents/models.py:1235 paperless_mail/models.py:274 #: documents/models.py:1235 paperless_mail/models.py:274
msgid "assign this tag" msgid "assign this tag"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-14 03:21+0000\n" "POT-Creation-Date: 2025-09-14 03:21+0000\n"
"PO-Revision-Date: 2025-09-17 00:33\n" "PO-Revision-Date: 2025-09-14 03:22\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese Simplified\n" "Language-Team: Chinese Simplified\n"
"Language: zh_CN\n" "Language: zh_CN\n"
@@ -750,7 +750,7 @@ msgstr "选择"
#: documents/models.py:762 #: documents/models.py:762
msgid "Long Text" msgid "Long Text"
msgstr "Good" msgstr ""
#: documents/models.py:774 #: documents/models.py:774
msgid "data type" msgid "data type"