Feature: select custom field type (#7167)

This commit is contained in:
shamoon
2024-07-09 07:57:07 -07:00
committed by GitHub
parent c03aa03ac2
commit 4ad4862641
16 changed files with 449 additions and 46 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.PositiveSmallIntegerField(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"))
MONETARY = ("monetary", _("Monetary"))
DOCUMENTLINK = ("documentlink", _("Document Link"))
SELECT = ("select", _("Select"))
created = models.DateTimeField(
_("created"),
@@ -825,6 +826,15 @@ class CustomField(models.Model):
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:
ordering = ("created",)
verbose_name = _("custom field")
@@ -888,6 +898,8 @@ class CustomFieldInstance(models.Model):
value_document_ids = models.JSONField(null=True)
value_select = models.PositiveSmallIntegerField(null=True)
class Meta:
ordering = ("created",)
verbose_name = _("custom field instance")
@@ -900,7 +912,12 @@ class CustomFieldInstance(models.Model):
]
def __str__(self) -> str:
return str(self.field.name) + f" : {self.value}"
value = (
self.field.extra_data["select_options"][self.value_select]
if self.field.data_type == CustomField.FieldDataType.SELECT
else self.value
)
return str(self.field.name) + f" : {value}"
@property
def value(self):
@@ -924,6 +941,8 @@ class CustomFieldInstance(models.Model):
return self.value_monetary
elif self.field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
return self.value_document_ids
elif self.field.data_type == CustomField.FieldDataType.SELECT:
return self.value_select
raise NotImplementedError(self.field.data_type)

View File

@@ -455,6 +455,7 @@ class CustomFieldSerializer(serializers.ModelSerializer):
"id",
"name",
"data_type",
"extra_data",
]
def validate(self, attrs):
@@ -476,6 +477,23 @@ class CustomFieldSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(
{"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 "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
for option in attrs["extra_data"]["select_options"]
)
)
):
raise serializers.ValidationError(
{"error": "extra_data.select_options must be a valid list"},
)
return super().validate(attrs)
@@ -507,6 +525,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
CustomField.FieldDataType.FLOAT: "value_float",
CustomField.FieldDataType.MONETARY: "value_monetary",
CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
CustomField.FieldDataType.SELECT: "value_select",
}
# An instance is attached to a document
document: Document = validated_data["document"]
@@ -563,6 +582,14 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
)(data["value"])
elif field.data_type == CustomField.FieldDataType.STRING:
MaxLengthValidator(limit_value=128)(data["value"])
elif field.data_type == CustomField.FieldDataType.SELECT:
select_options = field.extra_data["select_options"]
try:
select_options[data["value"]]
except Exception:
raise serializers.ValidationError(
f"Value must be index of an element in {select_options}",
)
return data

View File

@@ -1,3 +1,4 @@
import json
from datetime import date
from django.contrib.auth.models import User
@@ -49,10 +50,31 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
data = resp.json()
self.assertEqual(len(data), 3)
self.assertEqual(data["name"], name)
self.assertEqual(data["data_type"], field_type)
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "select",
"name": "Select Field",
"extra_data": {
"select_options": ["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"]["select_options"],
["Option 1", "Option 2"],
)
def test_create_custom_field_nonunique_name(self):
"""
GIVEN:
@@ -76,6 +98,45 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_custom_field_select_invalid_options(self):
"""
GIVEN:
- Custom field does not exist
WHEN:
- API request to create custom field with invalid select options
THEN:
- HTTP 400 is returned
"""
# Not a list
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "select",
"name": "Select Field",
"extra_data": {
"select_options": "not a list",
},
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
# No options
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "select",
"name": "Select Field",
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_custom_field_instance(self):
"""
GIVEN:
@@ -135,6 +196,13 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
name="Test Custom Field Doc Link",
data_type=CustomField.FieldDataType.DOCUMENTLINK,
)
custom_field_select = CustomField.objects.create(
name="Test Custom Field Select",
data_type=CustomField.FieldDataType.SELECT,
extra_data={
"select_options": ["Option 1", "Option 2"],
},
)
date_value = date.today()
@@ -178,6 +246,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
"field": custom_field_documentlink.id,
"value": [doc2.id],
},
{
"field": custom_field_select.id,
"value": 0,
},
],
},
format="json",
@@ -199,11 +271,12 @@ 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},
],
)
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):
"""
@@ -568,6 +641,44 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(CustomFieldInstance.objects.count(), 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={
"select_options": ["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):
"""
GIVEN: