Basic backend implementation of select custom field type

This commit is contained in:
shamoon 2024-07-03 20:48:02 -07:00
parent c03aa03ac2
commit 7c7b722264
4 changed files with 144 additions and 2 deletions

View File

@ -0,0 +1,48 @@
# Generated by Django 4.2.13 on 2024-07-04 01:02
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1049_document_deleted_at_document_restored_at"),
]
operations = [
migrations.AddField(
model_name="customfield",
name="extra_data",
field=models.JSONField(
blank=True,
help_text="Extra data for the custom field, such as select options",
null=True,
verbose_name="extra data",
),
),
migrations.AddField(
model_name="customfieldinstance",
name="value_select",
field=models.IntegerField(null=True),
),
migrations.AlterField(
model_name="customfield",
name="data_type",
field=models.CharField(
choices=[
("string", "String"),
("url", "URL"),
("date", "Date"),
("boolean", "Boolean"),
("integer", "Integer"),
("float", "Float"),
("monetary", "Monetary"),
("documentlink", "Document Link"),
("select", "Select"),
],
editable=False,
max_length=50,
verbose_name="data type",
),
),
]

View File

@ -808,6 +808,7 @@ class CustomField(models.Model):
FLOAT = ("float", _("Float")) FLOAT = ("float", _("Float"))
MONETARY = ("monetary", _("Monetary")) MONETARY = ("monetary", _("Monetary"))
DOCUMENTLINK = ("documentlink", _("Document Link")) DOCUMENTLINK = ("documentlink", _("Document Link"))
SELECT = ("select", _("Select"))
created = models.DateTimeField( created = models.DateTimeField(
_("created"), _("created"),
@ -825,6 +826,15 @@ class CustomField(models.Model):
editable=False, editable=False,
) )
extra_data = models.JSONField(
_("extra data"),
null=True,
blank=True,
help_text=_(
"Extra data for the custom field, such as select options",
),
)
class Meta: class Meta:
ordering = ("created",) ordering = ("created",)
verbose_name = _("custom field") verbose_name = _("custom field")
@ -888,6 +898,8 @@ class CustomFieldInstance(models.Model):
value_document_ids = models.JSONField(null=True) value_document_ids = models.JSONField(null=True)
value_select = models.IntegerField(null=True)
class Meta: class Meta:
ordering = ("created",) ordering = ("created",)
verbose_name = _("custom field instance") verbose_name = _("custom field instance")
@ -924,6 +936,8 @@ class CustomFieldInstance(models.Model):
return self.value_monetary return self.value_monetary
elif self.field.data_type == CustomField.FieldDataType.DOCUMENTLINK: elif self.field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
return self.value_document_ids return self.value_document_ids
elif self.field.data_type == CustomField.FieldDataType.SELECT:
return self.value_select
raise NotImplementedError(self.field.data_type) raise NotImplementedError(self.field.data_type)

View File

@ -455,6 +455,7 @@ class CustomFieldSerializer(serializers.ModelSerializer):
"id", "id",
"name", "name",
"data_type", "data_type",
"extra_data",
] ]
def validate(self, attrs): def validate(self, attrs):
@ -476,6 +477,14 @@ class CustomFieldSerializer(serializers.ModelSerializer):
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "Object violates name unique constraint"}, {"error": "Object violates name unique constraint"},
) )
if (
"data_type" in attrs
and attrs["data_type"] == CustomField.FieldDataType.SELECT
and ("extra_data" not in attrs or not isinstance(attrs["extra_data"], list))
):
raise serializers.ValidationError(
{"error": "extra_data must be a list"},
)
return super().validate(attrs) return super().validate(attrs)
@ -507,6 +516,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
CustomField.FieldDataType.FLOAT: "value_float", CustomField.FieldDataType.FLOAT: "value_float",
CustomField.FieldDataType.MONETARY: "value_monetary", CustomField.FieldDataType.MONETARY: "value_monetary",
CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids", CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
CustomField.FieldDataType.SELECT: "value_select",
} }
# An instance is attached to a document # An instance is attached to a document
document: Document = validated_data["document"] document: Document = validated_data["document"]
@ -563,6 +573,13 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
)(data["value"]) )(data["value"])
elif field.data_type == CustomField.FieldDataType.STRING: elif field.data_type == CustomField.FieldDataType.STRING:
MaxLengthValidator(limit_value=128)(data["value"]) MaxLengthValidator(limit_value=128)(data["value"])
elif field.data_type == CustomField.FieldDataType.SELECT:
try:
field.extra_data[data["value"]]
except Exception:
raise serializers.ValidationError(
f"Value must be index of an element in {field.extra_data}",
)
return data return data

View File

@ -1,3 +1,4 @@
import json
from datetime import date from datetime import date
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -49,10 +50,26 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
data = resp.json() data = resp.json()
self.assertEqual(len(data), 3)
self.assertEqual(data["name"], name) self.assertEqual(data["name"], name)
self.assertEqual(data["data_type"], field_type) self.assertEqual(data["data_type"], field_type)
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "select",
"name": "Select Field",
"extra_data": ["Option 1", "Option 2"],
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
data = resp.json()
self.assertCountEqual(data["extra_data"], ["Option 1", "Option 2"])
def test_create_custom_field_nonunique_name(self): def test_create_custom_field_nonunique_name(self):
""" """
GIVEN: GIVEN:
@ -135,6 +152,11 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
name="Test Custom Field Doc Link", name="Test Custom Field Doc Link",
data_type=CustomField.FieldDataType.DOCUMENTLINK, data_type=CustomField.FieldDataType.DOCUMENTLINK,
) )
custom_field_select = CustomField.objects.create(
name="Test Custom Field Select",
data_type=CustomField.FieldDataType.SELECT,
extra_data=["Option 1", "Option 2"],
)
date_value = date.today() date_value = date.today()
@ -178,6 +200,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
"field": custom_field_documentlink.id, "field": custom_field_documentlink.id,
"value": [doc2.id], "value": [doc2.id],
}, },
{
"field": custom_field_select.id,
"value": 0,
},
], ],
}, },
format="json", format="json",
@ -199,11 +225,12 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
{"field": custom_field_monetary.id, "value": "EUR11.10"}, {"field": custom_field_monetary.id, "value": "EUR11.10"},
{"field": custom_field_monetary2.id, "value": "11.1"}, {"field": custom_field_monetary2.id, "value": "11.1"},
{"field": custom_field_documentlink.id, "value": [doc2.id]}, {"field": custom_field_documentlink.id, "value": [doc2.id]},
{"field": custom_field_select.id, "value": 0},
], ],
) )
doc.refresh_from_db() doc.refresh_from_db()
self.assertEqual(len(doc.custom_fields.all()), 9) self.assertEqual(len(doc.custom_fields.all()), 10)
def test_change_custom_field_instance_value(self): def test_change_custom_field_instance_value(self):
""" """
@ -568,6 +595,42 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(CustomFieldInstance.objects.count(), 0) self.assertEqual(CustomFieldInstance.objects.count(), 0)
self.assertEqual(len(doc.custom_fields.all()), 0) self.assertEqual(len(doc.custom_fields.all()), 0)
def test_custom_field_value_select_validation(self):
"""
GIVEN:
- Document & custom field exist
WHEN:
- API request to set a field value to something not in the select options
THEN:
- HTTP 400 is returned
- No field instance is created or attached to the document
"""
doc = Document.objects.create(
title="WOW",
content="the content",
checksum="123",
mime_type="application/pdf",
)
custom_field_select = CustomField.objects.create(
name="Test Custom Field SELECT",
data_type=CustomField.FieldDataType.SELECT,
extra_data=["Option 1", "Option 2"],
)
resp = self.client.patch(
f"/api/documents/{doc.id}/",
data={
"custom_fields": [
{"field": custom_field_select.id, "value": 3},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(CustomFieldInstance.objects.count(), 0)
self.assertEqual(len(doc.custom_fields.all()), 0)
def test_custom_field_not_null(self): def test_custom_field_not_null(self):
""" """
GIVEN: GIVEN: