name: 'Field 4',
data_type: CustomFieldDataType.Select,
extra_data: {
- select_options: ['Option 1', 'Option 2', 'Option 3'],
+ select_options: [
+ { label: 'Option 1', id: 'abc-123' },
+ { label: 'Option 2', id: 'def-456' },
+ { label: 'Option 3', id: 'ghi-789' },
+ ],
},
},
{
})
it('should show select value', () => {
- expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
+ expect(component.getSelectValue(customFields[3], 'ghi-789')).toEqual(
+ 'Option 3'
+ )
})
})
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
}
- public getSelectValue(field: CustomField, index: number): string {
- return field.extra_data.select_options[index]
+ public getSelectValue(field: CustomField, id: string): string {
+ return field.extra_data.select_options?.find((o) => o.id === id)?.label
}
ngOnDestroy(): void {
<ng-select #fieldSelects
class="paperless-input-select rounded-end"
[items]="getSelectOptionsForField(atom.field)"
+ bindLabel="label"
+ bindValue="id"
[(ngModel)]="atom.value"
[disabled]="disabled"
(mousedown)="$event.stopImmediatePropagation()"
<ng-select
class="paperless-input-select rounded-end"
[items]="getSelectOptionsForField(atom.field)"
+ bindLabel="label"
+ bindValue="id"
[(ngModel)]="atom.value"
[disabled]="disabled"
[multiple]="true"
id: 2,
name: 'Test Select Field',
data_type: CustomFieldDataType.Select,
- extra_data: { select_options: ['Option 1', 'Option 2'] },
+ extra_data: {
+ select_options: [
+ { label: 'Option 1', id: 'abc-123' },
+ { label: 'Option 2', id: 'def-456' },
+ ],
+ },
},
]
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.Select,
- extra_data: { select_options: ['Option 1', 'Option 2'] },
+ extra_data: {
+ select_options: [
+ { label: 'Option 1', id: 'abc-123' },
+ { label: 'Option 2', id: 'def-456' },
+ ],
+ },
}
component.customFields = [field]
const options = component.getSelectOptionsForField(1)
- expect(options).toEqual(['Option 1', 'Option 2'])
+ expect(options).toEqual([
+ { label: 'Option 1', id: 'abc-123' },
+ { label: 'Option 2', id: 'def-456' },
+ ])
// Fallback to empty array if field is not found
const options2 = component.getSelectOptionsForField(2)
}))
}
- getSelectOptionsForField(fieldID: number): string[] {
+ getSelectOptionsForField(
+ fieldID: number
+ ): Array<{ label: string; id: string }> {
const field = this.customFields.find((field) => field.id === fieldID)
if (field) {
return field.extra_data['select_options']
</button>
<div formArrayName="select_options">
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
- <div class="input-group input-group-sm my-2">
- <input #selectOption type="text" class="form-control" [formControl]="option" autocomplete="off">
+ <div class="input-group input-group-sm my-2" [formGroup]="objectForm.controls.extra_data.controls.select_options.controls[i]">
+ <input #selectOption type="text" class="form-control" formControlName="label" autocomplete="off">
+ <input type="hidden" formControlName="id">
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
</div>
}
name: 'Field 1',
data_type: CustomFieldDataType.Select,
extra_data: {
- select_options: ['Option 1', 'Option 2', 'Option 3'],
+ select_options: [
+ { label: 'Option 1', id: '123-xyz' },
+ { label: 'Option 2', id: '456-abc' },
+ { label: 'Option 3', id: '789-123' },
+ ],
},
}
fixture.detectChanges()
component.ngOnInit()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
- ).toBe(1)
+ ).toBe(0)
component.addSelectOption()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
- ).toBe(2)
+ ).toBe(1)
component.addSelectOption()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
- ).toBe(3)
+ ).toBe(2)
component.removeSelectOption(0)
expect(
component.objectForm.get('extra_data').get('select_options').value.length
- ).toBe(2)
+ ).toBe(1)
})
it('should focus on last select option input', () => {
}
if (this.object?.data_type === CustomFieldDataType.Select) {
this.selectOptions.clear()
- this.object.extra_data.select_options.forEach((option) =>
- this.selectOptions.push(new FormControl(option))
- )
+ this.object.extra_data.select_options
+ .filter((option) => option)
+ .forEach((option) =>
+ this.selectOptions.push(
+ new FormGroup({
+ label: new FormControl(option.label),
+ id: new FormControl(option.id),
+ })
+ )
+ )
}
}
name: new FormControl(null),
data_type: new FormControl(null),
extra_data: new FormGroup({
- select_options: new FormArray([new FormControl(null)]),
+ select_options: new FormArray([]),
default_currency: new FormControl(null),
}),
})
}
public addSelectOption() {
- this.selectOptions.push(new FormControl(''))
+ this.selectOptions.push(
+ new FormGroup({ label: new FormControl(null), id: new FormControl(null) })
+ )
}
public removeSelectOption(index: number) {
const expectedTitle = `Filter documents with this ${component.title}`
expect(component.filterButtonTitle).toEqual(expectedTitle)
})
-
- it('should support setting items as a plain array', () => {
- component.itemsArray = ['foo', 'bar']
- expect(component.items).toEqual([
- { id: 0, name: 'foo' },
- { id: 1, name: 'bar' },
- ])
- })
})
if (items && this.value) this.checkForPrivateItems(this.value)
}
- @Input()
- set itemsArray(items: any[]) {
- this._items = items.map((item, index) => ({ id: index, name: item }))
- }
-
writeValue(newValue: any): void {
if (newValue && this._items) {
this.checkForPrivateItems(newValue)
@case (CustomFieldDataType.Select) {
<pngx-input-select formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
- [itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
+ [items]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
+ bindLabel="label"
[allowNull]="true"
[horizontal]="true"
[removable]="userIsOwner"
name: string
created?: Date
extra_data?: {
- select_options?: string[]
+ select_options?: Array<{ label: string; id: string }>
default_currency?: string
}
document_count?: number
if fields_with_matching_selects.count() > 0:
for field in fields_with_matching_selects:
options = field.extra_data.get("select_options", [])
- for index, option in enumerate(options):
- if option.lower().find(value.lower()) != -1:
- option_ids.extend([index])
+ for _, option in enumerate(options):
+ if option.get("label").lower().find(value.lower()) != -1:
+ option_ids.extend([option.get("id")])
return (
qs.filter(custom_fields__field__name__icontains=value)
| qs.filter(custom_fields__value_text__icontains=value)
return qs
-class SelectField(serializers.IntegerField):
+class SelectField(serializers.CharField):
def __init__(self, custom_field: CustomField):
self._options = custom_field.extra_data["select_options"]
- super().__init__(min_value=0, max_value=len(self._options))
+ super().__init__(max_length=16)
def to_internal_value(self, data):
- if not isinstance(data, int):
- # If the supplied value is not an integer,
- # we will try to map it to an option index.
- try:
- data = self._options.index(data)
- except ValueError:
- pass
+ # If the supplied value is the option label instead of the ID
+ try:
+ data = next(
+ option.get("id")
+ for option in self._options
+ if option.get("label") == data
+ )
+ except StopIteration:
+ pass
return super().to_internal_value(data)
from documents.settings import EXPORTER_CRYPTO_SETTINGS_NAME
from documents.settings import EXPORTER_FILE_NAME
from documents.settings import EXPORTER_THUMBNAIL_NAME
-from documents.signals.handlers import update_cf_instance_documents
+from documents.signals.handlers import check_paths_and_prune_custom_fields
from documents.signals.handlers import update_filename_and_move_files
from documents.utils import copy_file_with_basic_stats
from paperless import version
),
disable_signal(
post_save,
- receiver=update_cf_instance_documents,
+ receiver=check_paths_and_prune_custom_fields,
sender=CustomField,
),
):
--- /dev/null
+# Generated by Django 5.1.1 on 2024-11-13 05:14
+
+from django.db import migrations
+from django.db import models
+from django.db import transaction
+from django.utils.crypto import get_random_string
+
+
+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.filter(
+ data_type="select",
+ ): # CustomField.FieldDataType.SELECT
+ old_select_options = custom_field.extra_data["select_options"]
+ custom_field.extra_data["select_options"] = [
+ {"id": get_random_string(16), "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.get("id") == instance.value_select
+ )
+ instance.save()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="customfieldinstance",
+ name="value_select",
+ field=models.CharField(max_length=16, null=True),
+ ),
+ migrations.RunPython(
+ migrate_customfield_selects,
+ reverse_migrate_customfield_selects,
+ ),
+ ]
value_document_ids = models.JSONField(null=True)
- value_select = models.PositiveSmallIntegerField(null=True)
+ value_select = models.CharField(null=True, max_length=16)
class Meta:
ordering = ("created",)
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
if (
"data_type" in attrs
and attrs["data_type"] == CustomField.FieldDataType.SELECT
- and (
+ ) or (
+ self.instance
+ and self.instance.data_type == CustomField.FieldDataType.SELECT
+ ):
+ if (
"extra_data" not in attrs
or "select_options" not in attrs["extra_data"]
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"]
)
- )
- ):
- raise serializers.ValidationError(
- {"error": "extra_data.select_options must be a valid list"},
- )
+ ):
+ raise serializers.ValidationError(
+ {"error": "extra_data.select_options must be a valid list"},
+ )
+ # labels are valid, generate ids if not present
+ for option in attrs["extra_data"]["select_options"]:
+ if option.get("id") is None:
+ option["id"] = get_random_string(length=16)
elif (
"data_type" in attrs
and attrs["data_type"] == CustomField.FieldDataType.MONETARY
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"]
pass
-# should be disabled in /src/documents/management/commands/document_importer.py handle
-@receiver(models.signals.post_save, sender=CustomField)
-def update_cf_instance_documents(sender, instance: CustomField, **kwargs):
- """
- 'Select' custom field instances get their end-user value (e.g. in file names) from the select_options in extra_data,
- which is contained in the custom field itself. So when the field is changed, we (may) need to update the file names
- of all documents that have this custom field.
- """
- if (
- instance.data_type == CustomField.FieldDataType.SELECT
- ): # Only select fields, for now
- for cf_instance in instance.fields.all():
- update_filename_and_move_files(sender, cf_instance)
-
-
# should be disabled in /src/documents/management/commands/document_importer.py handle
@receiver(models.signals.post_save, sender=CustomFieldInstance)
@receiver(models.signals.m2m_changed, sender=Document.tags.through)
)
+# should be disabled in /src/documents/management/commands/document_importer.py handle
+@receiver(models.signals.post_save, sender=CustomField)
+def check_paths_and_prune_custom_fields(sender, instance: CustomField, **kwargs):
+ """
+ When a custom field is updated:
+ 1. 'Select' custom field instances get their end-user value (e.g. in file names) from the select_options in extra_data,
+ which is contained in the custom field itself. So when the field is changed, we (may) need to update the file names
+ of all documents that have this custom field.
+ 2. If a 'Select' field option was removed, we need to nullify the custom field instances that have the option.
+ """
+ if (
+ instance.data_type == CustomField.FieldDataType.SELECT
+ ): # Only select fields, for now
+ for cf_instance in instance.fields.all():
+ options = instance.extra_data.get("select_options", [])
+ try:
+ next(
+ option["label"]
+ for option in options
+ if option["id"] == cf_instance.value
+ )
+ except StopIteration:
+ # The value of this custom field instance is not in the select options anymore
+ cf_instance.value_select = None
+ cf_instance.save()
+ update_filename_and_move_files(sender, cf_instance)
+
+
def set_log_entry(sender, document: Document, logging_group=None, **kwargs):
ct = ContentType.objects.get(model="document")
user = User.objects.get(username="consumer")
):
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:
import json
from datetime import date
+from unittest.mock import ANY
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
"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"},
+ ],
},
},
),
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):
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+ def test_custom_field_select_unique_ids(self):
+ """
+ GIVEN:
+ - Nothing
+ - Existing custom field
+ WHEN:
+ - API request to create custom field with select options without id
+ THEN:
+ - Unique ids are generated for each option
+ """
+ resp = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "data_type": "select",
+ "name": "Select Field",
+ "extra_data": {
+ "select_options": [
+ {"label": "Option 1"},
+ {"label": "Option 2"},
+ ],
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
+
+ data = resp.json()
+
+ self.assertCountEqual(
+ data["extra_data"]["select_options"],
+ [
+ {"label": "Option 1", "id": ANY},
+ {"label": "Option 2", "id": ANY},
+ ],
+ )
+
+ # Add a new option
+ resp = self.client.patch(
+ f"{self.ENDPOINT}{data['id']}/",
+ json.dumps(
+ {
+ "extra_data": {
+ "select_options": data["extra_data"]["select_options"]
+ + [{"label": "Option 3"}],
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+
+ data = resp.json()
+
+ self.assertCountEqual(
+ data["extra_data"]["select_options"],
+ [
+ {"label": "Option 1", "id": ANY},
+ {"label": "Option 2", "id": ANY},
+ {"label": "Option 3", "id": ANY},
+ ],
+ )
+
+ def test_custom_field_select_options_pruned(self):
+ """
+ GIVEN:
+ - Select custom field exists and document instance with one of the options
+ WHEN:
+ - API request to remove an option from the select field
+ THEN:
+ - The option is removed from the field
+ - The option is removed from the document instance
+ """
+ custom_field_select = CustomField.objects.create(
+ name="Select Field",
+ data_type=CustomField.FieldDataType.SELECT,
+ extra_data={
+ "select_options": [
+ {"label": "Option 1", "id": "abc-123"},
+ {"label": "Option 2", "id": "def-456"},
+ {"label": "Option 3", "id": "ghi-789"},
+ ],
+ },
+ )
+
+ doc = Document.objects.create(
+ title="WOW",
+ content="the content",
+ checksum="123",
+ mime_type="application/pdf",
+ )
+ CustomFieldInstance.objects.create(
+ document=doc,
+ field=custom_field_select,
+ value_text="abc-123",
+ )
+
+ resp = self.client.patch(
+ f"{self.ENDPOINT}{custom_field_select.id}/",
+ json.dumps(
+ {
+ "extra_data": {
+ "select_options": [
+ {"label": "Option 1", "id": "abc-123"},
+ {"label": "Option 3", "id": "ghi-789"},
+ ],
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+
+ data = resp.json()
+
+ self.assertCountEqual(
+ data["extra_data"]["select_options"],
+ [
+ {"label": "Option 1", "id": "abc-123"},
+ {"label": "Option 3", "id": "ghi-789"},
+ ],
+ )
+
+ doc.refresh_from_db()
+ self.assertEqual(doc.custom_fields.first().value, None)
+
def test_create_custom_field_monetary_validation(self):
"""
GIVEN:
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"},
+ ],
},
)
},
{
"field": custom_field_select.id,
- "value": 0,
+ "value": "abc-123",
},
],
},
{"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"},
],
)
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"},
+ ],
},
)
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",
name="Test Custom Field Select",
data_type=CustomField.FieldDataType.SELECT,
extra_data={
- "select_options": ["Option 1", "Choice 2"],
+ "select_options": [
+ {"label": "Option 1", "id": "abc123"},
+ {"label": "Choice 2", "id": "def456"},
+ ],
},
)
CustomFieldInstance.objects.create(
document=doc1,
field=custom_field_select,
- value_select=1,
+ value_select="def456",
)
r = self.client.get("/api/documents/?custom_fields__icontains=choice")
# Add some options to the select_field
select = self.custom_fields["select_field"]
- select.extra_data = {"select_options": ["A", "B", "C"]}
+ select.extra_data = {
+ "select_options": [
+ {"label": "A", "id": "abc-123"},
+ {"label": "B", "id": "def-456"},
+ {"label": "C", "id": "ghi-789"},
+ ],
+ }
select.save()
# Now we will create some test documents
# CustomField.FieldDataType.SELECT
self._create_document(select_field=None)
- self._create_document(select_field=0)
- self._create_document(select_field=1)
- self._create_document(select_field=2)
+ self._create_document(select_field="abc-123")
+ self._create_document(select_field="def-456")
+ self._create_document(select_field="ghi-789")
def _create_document(self, **kwargs):
title = str(kwargs)
)
def test_select(self):
- # For select fields, you can either specify the index
+ # For select fields, you can either specify the id of the option
# or the name of the option. They function exactly the same.
self._assert_query_match_predicate(
- ["select_field", "exact", 1],
+ ["select_field", "exact", "def-456"],
lambda document: "select_field" in document
- and document["select_field"] == 1,
+ and document["select_field"] == "def-456",
)
# This is the same as:
self._assert_query_match_predicate(
["select_field", "exact", "B"],
lambda document: "select_field" in document
- and document["select_field"] == 1,
+ and document["select_field"] == "def-456",
)
# ==========================================================#
def test_invalid_value(self):
self._assert_validation_error(
- json.dumps(["select_field", "exact", "not an option"]),
+ json.dumps(["select_field", "exact", []]),
["custom_field_query", "2"],
- "integer",
+ "string",
)
def test_invalid_logical_operator(self):
name="test",
data_type=CustomField.FieldDataType.SELECT,
extra_data={
- "select_options": ["apple", "banana", "cherry"],
+ "select_options": [
+ {"label": "apple", "id": "abc123"},
+ {"label": "banana", "id": "def456"},
+ {"label": "cherry", "id": "ghi789"},
+ ],
},
)
doc = Document.objects.create(
archive_checksum="B",
mime_type="application/pdf",
)
- CustomFieldInstance.objects.create(field=cf, document=doc, value_select=0)
+ CustomFieldInstance.objects.create(
+ field=cf,
+ document=doc,
+ value_select="abc123",
+ )
self.assertEqual(generate_filename(doc), "document_apple.pdf")
# handler should not have been called
self.assertEqual(m.call_count, 0)
cf.extra_data = {
- "select_options": ["aubergine", "banana", "cherry"],
+ "select_options": [
+ {"label": "aubergine", "id": "abc123"},
+ {"label": "banana", "id": "def456"},
+ {"label": "cherry", "id": "ghi789"},
+ ],
}
cf.save()
self.assertEqual(generate_filename(doc), "document_aubergine.pdf")
cf2 = CustomField.objects.create(
name="Select Field",
data_type=CustomField.FieldDataType.SELECT,
- extra_data={"select_options": ["ChoiceOne", "ChoiceTwo"]},
+ extra_data={
+ "select_options": [
+ {"label": "ChoiceOne", "id": "abc=123"},
+ {"label": "ChoiceTwo", "id": "def-456"},
+ ],
+ },
)
cfi1 = CustomFieldInstance.objects.create(
document=doc_a,
field=cf2,
- value_select=0,
+ value_select="abc=123",
)
cfi = CustomFieldInstance.objects.create(
--- /dev/null
+from unittest.mock import ANY
+
+from documents.tests.utils import TestMigrations
+
+
+class TestMigrateCustomFieldSelects(TestMigrations):
+ migrate_from = "1058_workflowtrigger_schedule_date_custom_field_and_more"
+ migrate_to = "1059_alter_customfieldinstance_value_select"
+
+ def setUpBeforeMigration(self, apps):
+ CustomField = apps.get_model("documents.CustomField")
+ self.old_format = CustomField.objects.create(
+ name="cf1",
+ data_type="select",
+ extra_data={"select_options": ["Option 1", "Option 2", "Option 3"]},
+ )
+ Document = apps.get_model("documents.Document")
+ doc = Document.objects.create(title="doc1")
+ CustomFieldInstance = apps.get_model("documents.CustomFieldInstance")
+ self.old_instance = CustomFieldInstance.objects.create(
+ field=self.old_format,
+ value_select=0,
+ document=doc,
+ )
+
+ def test_migrate_old_to_new_select_fields(self):
+ self.old_format.refresh_from_db()
+ self.old_instance.refresh_from_db()
+
+ self.assertEqual(
+ self.old_format.extra_data["select_options"],
+ [
+ {"label": "Option 1", "id": ANY},
+ {"label": "Option 2", "id": ANY},
+ {"label": "Option 3", "id": ANY},
+ ],
+ )
+
+ self.assertEqual(
+ self.old_instance.value_select,
+ self.old_format.extra_data["select_options"][0]["id"],
+ )
+
+
+class TestMigrationCustomFieldSelectsReverse(TestMigrations):
+ migrate_from = "1059_alter_customfieldinstance_value_select"
+ migrate_to = "1058_workflowtrigger_schedule_date_custom_field_and_more"
+
+ def setUpBeforeMigration(self, apps):
+ CustomField = apps.get_model("documents.CustomField")
+ self.new_format = CustomField.objects.create(
+ name="cf1",
+ data_type="select",
+ extra_data={
+ "select_options": [
+ {"label": "Option 1", "id": "id1"},
+ {"label": "Option 2", "id": "id2"},
+ {"label": "Option 3", "id": "id3"},
+ ],
+ },
+ )
+ Document = apps.get_model("documents.Document")
+ doc = Document.objects.create(title="doc1")
+ CustomFieldInstance = apps.get_model("documents.CustomFieldInstance")
+ self.new_instance = CustomFieldInstance.objects.create(
+ field=self.new_format,
+ value_select="id1",
+ document=doc,
+ )
+
+ def test_migrate_new_to_old_select_fields(self):
+ self.new_format.refresh_from_db()
+ self.new_instance.refresh_from_db()
+
+ self.assertEqual(
+ self.new_format.extra_data["select_options"],
+ [
+ "Option 1",
+ "Option 2",
+ "Option 3",
+ ],
+ )
+
+ self.assertEqual(
+ self.new_instance.value_select,
+ 0,
+ )