Enhancement: symmetric document links (#4907)

This commit is contained in:
shamoon 2023-12-19 13:43:50 -08:00 committed by GitHub
parent 5e8de4c1da
commit 638d9970fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 207 additions and 3 deletions

View File

@ -345,7 +345,7 @@ The following custom field types are supported:
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: float number with exactly two decimals, e.g. 12.30
- `Document Link`: reference(s) to other document(s), displayed as links
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
## Share Links

View File

@ -471,6 +471,10 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
# This key must exist, as it is validated
data_store_name = type_to_data_store_name_map[custom_field.data_type]
if custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
# prior to update so we can look for any docs that are going to be removed
self.reflect_doclinks(document, custom_field, validated_data["value"])
# Actually update or create the instance, providing the value
# to fill in the correct attribute based on the type
instance, _ = CustomFieldInstance.objects.update_or_create(
@ -494,6 +498,77 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
URLValidator()(data["value"])
return data
def reflect_doclinks(
self,
document: Document,
field: CustomField,
target_doc_ids: list[int],
):
"""
Add or remove 'symmetrical' links to `document` on all `target_doc_ids`
"""
# Check if any documents are going to be removed from the current list of links and remove the symmetrical links
current_field_instance = CustomFieldInstance.objects.filter(
field=field,
document=document,
).first()
if current_field_instance is not None:
for doc_id in current_field_instance.value:
if doc_id not in target_doc_ids:
self.remove_doclink(document, field, doc_id)
# Create an instance if target doc doesnt have this field or append it to an existing one
existing_custom_field_instances = {
custom_field.document_id: custom_field
for custom_field in CustomFieldInstance.objects.filter(
field=field,
document_id__in=target_doc_ids,
)
}
custom_field_instances_to_create = []
custom_field_instances_to_update = []
for target_doc_id in target_doc_ids:
target_doc_field_instance = existing_custom_field_instances.get(
target_doc_id,
)
if target_doc_field_instance is None:
custom_field_instances_to_create.append(
CustomFieldInstance(
document_id=target_doc_id,
field=field,
value_document_ids=[document.id],
),
)
elif document.id not in target_doc_field_instance.value:
target_doc_field_instance.value_document_ids.append(document.id)
custom_field_instances_to_update.append(target_doc_field_instance)
CustomFieldInstance.objects.bulk_create(custom_field_instances_to_create)
CustomFieldInstance.objects.bulk_update(
custom_field_instances_to_update,
["value_document_ids"],
)
@staticmethod
def remove_doclink(
document: Document,
field: CustomField,
target_doc_id: int,
):
"""
Removes a 'symmetrical' link to `document` from the target document's existing custom field instance
"""
target_doc_field_instance = CustomFieldInstance.objects.filter(
document_id=target_doc_id,
field=field,
).first()
if (
target_doc_field_instance is not None
and document.id in target_doc_field_instance.value
):
target_doc_field_instance.value.remove(document.id)
target_doc_field_instance.save()
class Meta:
model = CustomFieldInstance
fields = [
@ -549,6 +624,21 @@ class DocumentSerializer(
instance.save()
if "created_date" in validated_data:
validated_data.pop("created_date")
if instance.custom_fields.count() > 0 and "custom_fields" in validated_data:
incoming_custom_fields = [
field["field"] for field in validated_data["custom_fields"]
]
for custom_field_instance in instance.custom_fields.filter(
field__data_type=CustomField.FieldDataType.DOCUMENTLINK,
):
if custom_field_instance.field not in incoming_custom_fields:
# Doc link field is being removed entirely
for doc_id in custom_field_instance.value:
CustomFieldInstanceSerializer.remove_doclink(
instance,
custom_field_instance.field,
doc_id,
)
super().update(instance, validated_data)
return instance

View File

@ -70,6 +70,12 @@ class TestCustomField(DirectoriesMixin, APITestCase):
checksum="123",
mime_type="application/pdf",
)
doc2 = Document.objects.create(
title="WOW2",
content="the content2",
checksum="1234",
mime_type="application/pdf",
)
custom_field_string = CustomField.objects.create(
name="Test Custom Field String",
data_type=CustomField.FieldDataType.STRING,
@ -139,7 +145,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
},
{
"field": custom_field_documentlink.id,
"value": [1, 2, 3],
"value": [doc2.id],
},
],
},
@ -160,7 +166,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
{"field": custom_field_url.id, "value": "https://example.com"},
{"field": custom_field_float.id, "value": 12.3456},
{"field": custom_field_monetary.id, "value": 11.10},
{"field": custom_field_documentlink.id, "value": [1, 2, 3]},
{"field": custom_field_documentlink.id, "value": [doc2.id]},
],
)
@ -393,3 +399,111 @@ class TestCustomField(DirectoriesMixin, APITestCase):
self.assertEqual(CustomFieldInstance.objects.count(), 0)
self.assertEqual(len(doc.custom_fields.all()), 0)
def test_bidirectional_doclink_fields(self):
"""
GIVEN:
- Existing document
WHEN:
- Doc links are added or removed
THEN:
- Symmetrical link is created or removed as expected
"""
doc1 = Document.objects.create(
title="WOW1",
content="1",
checksum="1",
mime_type="application/pdf",
)
doc2 = Document.objects.create(
title="WOW2",
content="the content2",
checksum="2",
mime_type="application/pdf",
)
doc3 = Document.objects.create(
title="WOW3",
content="the content3",
checksum="3",
mime_type="application/pdf",
)
doc4 = Document.objects.create(
title="WOW4",
content="the content4",
checksum="4",
mime_type="application/pdf",
)
custom_field_doclink = CustomField.objects.create(
name="Test Custom Field Doc Link",
data_type=CustomField.FieldDataType.DOCUMENTLINK,
)
# Add links, creates bi-directional
resp = self.client.patch(
f"/api/documents/{doc1.id}/",
data={
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [2, 3, 4],
},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(CustomFieldInstance.objects.count(), 4)
self.assertEqual(doc2.custom_fields.first().value, [1])
self.assertEqual(doc3.custom_fields.first().value, [1])
self.assertEqual(doc4.custom_fields.first().value, [1])
# Add links appends if necessary
resp = self.client.patch(
f"/api/documents/{doc3.id}/",
data={
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [1, 4],
},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(doc4.custom_fields.first().value, [1, 3])
# Remove one of the links, removed on other doc
resp = self.client.patch(
f"/api/documents/{doc1.id}/",
data={
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [2, 3],
},
],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(doc2.custom_fields.first().value, [1])
self.assertEqual(doc3.custom_fields.first().value, [1, 4])
self.assertEqual(doc4.custom_fields.first().value, [3])
# Removes the field entirely
resp = self.client.patch(
f"/api/documents/{doc1.id}/",
data={
"custom_fields": [],
},
format="json",
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(doc2.custom_fields.first().value, [])
self.assertEqual(doc3.custom_fields.first().value, [4])
self.assertEqual(doc4.custom_fields.first().value, [3])