mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-16 21:55:37 -05:00
Enhancement: stable IDs for custom field select values
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-13 05:14
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
def migrate_customfield_selects(apps, schema_editor):
|
||||
"""
|
||||
Migrate the custom field selects from a simple list of strings to a list of dictionaries with
|
||||
label and id. Then update all instances of the custom field to use the new format.
|
||||
"""
|
||||
CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance")
|
||||
CustomField = apps.get_model("documents", "CustomField")
|
||||
|
||||
with transaction.atomic():
|
||||
for custom_field in CustomField.objects.all():
|
||||
if custom_field.data_type == "select": # CustomField.FieldDataType.SELECT
|
||||
old_select_options = custom_field.extra_data["select_options"]
|
||||
custom_field.extra_data["select_options"] = [
|
||||
{"id": str(uuid.uuid4()), "label": value}
|
||||
for value in old_select_options
|
||||
]
|
||||
custom_field.save()
|
||||
|
||||
for instance in CustomFieldInstance.objects.filter(field=custom_field):
|
||||
if instance.value_select:
|
||||
instance.value_select = custom_field.extra_data[
|
||||
"select_options"
|
||||
][int(instance.value_select)]["id"]
|
||||
instance.save()
|
||||
|
||||
|
||||
def reverse_migrate_customfield_selects(apps, schema_editor):
|
||||
"""
|
||||
Reverse the migration of the custom field selects from a list of dictionaries with label and id
|
||||
to a simple list of strings. Then update all instances of the custom field to use the old format,
|
||||
which is just the index of the selected option.
|
||||
"""
|
||||
CustomFieldInstance = apps.get_model("documents", "CustomFieldInstance")
|
||||
CustomField = apps.get_model("documents", "CustomField")
|
||||
|
||||
with transaction.atomic():
|
||||
for custom_field in CustomField.objects.all():
|
||||
if custom_field.data_type == "select": # CustomField.FieldDataType.SELECT
|
||||
old_select_options = custom_field.extra_data["select_options"]
|
||||
custom_field.extra_data["select_options"] = [
|
||||
option["label"]
|
||||
for option in custom_field.extra_data["select_options"]
|
||||
]
|
||||
custom_field.save()
|
||||
|
||||
for instance in CustomFieldInstance.objects.filter(field=custom_field):
|
||||
instance.value_select = next(
|
||||
index
|
||||
for index, option in enumerate(old_select_options)
|
||||
if option.id == instance.value_select
|
||||
)
|
||||
instance.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1056_customfieldinstance_deleted_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="customfieldinstance",
|
||||
name="value_select",
|
||||
field=models.CharField(max_length=128, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
migrate_customfield_selects,
|
||||
reverse_migrate_customfield_selects,
|
||||
),
|
||||
]
|
@@ -947,7 +947,7 @@ class CustomFieldInstance(SoftDeleteModel):
|
||||
|
||||
value_document_ids = models.JSONField(null=True)
|
||||
|
||||
value_select = models.PositiveSmallIntegerField(null=True)
|
||||
value_select = models.CharField(null=True, max_length=128)
|
||||
|
||||
class Meta:
|
||||
ordering = ("created",)
|
||||
@@ -962,7 +962,11 @@ class CustomFieldInstance(SoftDeleteModel):
|
||||
|
||||
def __str__(self) -> str:
|
||||
value = (
|
||||
self.field.extra_data["select_options"][self.value_select]
|
||||
next(
|
||||
option.get("label")
|
||||
for option in self.field.extra_data["select_options"]
|
||||
if option.get("id") == self.value_select
|
||||
)
|
||||
if (
|
||||
self.field.data_type == CustomField.FieldDataType.SELECT
|
||||
and self.value_select is not None
|
||||
|
@@ -539,7 +539,7 @@ class CustomFieldSerializer(serializers.ModelSerializer):
|
||||
or not isinstance(attrs["extra_data"]["select_options"], list)
|
||||
or len(attrs["extra_data"]["select_options"]) == 0
|
||||
or not all(
|
||||
isinstance(option, str) and len(option) > 0
|
||||
len(option.get("label", "")) > 0
|
||||
for option in attrs["extra_data"]["select_options"]
|
||||
)
|
||||
)
|
||||
@@ -646,10 +646,14 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
||||
elif field.data_type == CustomField.FieldDataType.SELECT:
|
||||
select_options = field.extra_data["select_options"]
|
||||
try:
|
||||
select_options[data["value"]]
|
||||
next(
|
||||
option
|
||||
for option in select_options
|
||||
if option["id"] == data["value"]
|
||||
)
|
||||
except Exception:
|
||||
raise serializers.ValidationError(
|
||||
f"Value must be index of an element in {select_options}",
|
||||
f"Value must be an id of an element in {select_options}",
|
||||
)
|
||||
elif field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
|
||||
doc_ids = data["value"]
|
||||
|
@@ -253,7 +253,11 @@ def get_custom_fields_context(
|
||||
):
|
||||
options = field_instance.field.extra_data["select_options"]
|
||||
value = pathvalidate.sanitize_filename(
|
||||
options[int(field_instance.value)],
|
||||
next(
|
||||
option["label"]
|
||||
for option in options
|
||||
if option["id"] == field_instance.value
|
||||
),
|
||||
replacement_text="-",
|
||||
)
|
||||
else:
|
||||
|
@@ -61,7 +61,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
"data_type": "select",
|
||||
"name": "Select Field",
|
||||
"extra_data": {
|
||||
"select_options": ["Option 1", "Option 2"],
|
||||
"select_options": [
|
||||
{"label": "Option 1", "id": "abc-123"},
|
||||
{"label": "Option 2", "id": "def-456"},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -73,7 +76,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
|
||||
self.assertCountEqual(
|
||||
data["extra_data"]["select_options"],
|
||||
["Option 1", "Option 2"],
|
||||
[
|
||||
{"label": "Option 1", "id": "abc-123"},
|
||||
{"label": "Option 2", "id": "def-456"},
|
||||
],
|
||||
)
|
||||
|
||||
def test_create_custom_field_nonunique_name(self):
|
||||
@@ -261,7 +267,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
name="Test Custom Field Select",
|
||||
data_type=CustomField.FieldDataType.SELECT,
|
||||
extra_data={
|
||||
"select_options": ["Option 1", "Option 2"],
|
||||
"select_options": [
|
||||
{"label": "Option 1", "id": "abc-123"},
|
||||
{"label": "Option 2", "id": "def-456"},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -309,7 +318,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
},
|
||||
{
|
||||
"field": custom_field_select.id,
|
||||
"value": 0,
|
||||
"value": "abc-123",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -332,7 +341,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
{"field": custom_field_monetary.id, "value": "EUR11.10"},
|
||||
{"field": custom_field_monetary2.id, "value": "11.1"},
|
||||
{"field": custom_field_documentlink.id, "value": [doc2.id]},
|
||||
{"field": custom_field_select.id, "value": 0},
|
||||
{"field": custom_field_select.id, "value": "abc-123"},
|
||||
],
|
||||
)
|
||||
|
||||
@@ -722,7 +731,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
name="Test Custom Field SELECT",
|
||||
data_type=CustomField.FieldDataType.SELECT,
|
||||
extra_data={
|
||||
"select_options": ["Option 1", "Option 2"],
|
||||
"select_options": [
|
||||
{"label": "Option 1", "id": "abc-123"},
|
||||
{"label": "Option 2", "id": "def-456"},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -730,7 +742,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
f"/api/documents/{doc.id}/",
|
||||
data={
|
||||
"custom_fields": [
|
||||
{"field": custom_field_select.id, "value": 3},
|
||||
{"field": custom_field_select.id, "value": "not an option"},
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
|
Reference in New Issue
Block a user