# 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(
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 = [
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
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,
},
{
"field": custom_field_documentlink.id,
- "value": [1, 2, 3],
+ "value": [doc2.id],
},
],
},
{"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]},
],
)
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])