Basic backend implementation of select custom field type
This commit is contained in:
parent
c03aa03ac2
commit
7c7b722264
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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.IntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ("created",)
|
||||
verbose_name = _("custom field instance")
|
||||
@ -924,6 +936,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)
|
||||
|
||||
|
||||
|
@ -455,6 +455,7 @@ class CustomFieldSerializer(serializers.ModelSerializer):
|
||||
"id",
|
||||
"name",
|
||||
"data_type",
|
||||
"extra_data",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
@ -476,6 +477,14 @@ 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 not isinstance(attrs["extra_data"], list))
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "extra_data must be a list"},
|
||||
)
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
@ -507,6 +516,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 +573,13 @@ 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:
|
||||
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
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@ -49,10 +50,26 @@ 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": ["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):
|
||||
"""
|
||||
GIVEN:
|
||||
@ -135,6 +152,11 @@ 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=["Option 1", "Option 2"],
|
||||
)
|
||||
|
||||
date_value = date.today()
|
||||
|
||||
@ -178,6 +200,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
|
||||
"field": custom_field_documentlink.id,
|
||||
"value": [doc2.id],
|
||||
},
|
||||
{
|
||||
"field": custom_field_select.id,
|
||||
"value": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
@ -199,11 +225,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 +595,42 @@ 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=["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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user