From d9526ea32fa2816a19a03a533003d14495318bde Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Fri, 27 Oct 2023 20:52:35 -0700 Subject: [PATCH] Save some not working stuff --- ...omfieldboolean_customfielddate_and_more.py | 124 +++++--- src/documents/models.py | 23 +- src/documents/serialisers.py | 47 ++- src/documents/tests/test_api_custom_fields.py | 82 +++++ .../tests/test_api_custom_metadata.py | 291 ------------------ src/documents/views.py | 96 +----- 6 files changed, 216 insertions(+), 447 deletions(-) create mode 100644 src/documents/tests/test_api_custom_fields.py delete mode 100644 src/documents/tests/test_api_custom_metadata.py diff --git a/src/documents/migrations/1040_customfield_customfieldboolean_customfielddate_and_more.py b/src/documents/migrations/1040_customfield_customfieldboolean_customfielddate_and_more.py index d3d7ebea6..39d4a019e 100644 --- a/src/documents/migrations/1040_customfield_customfieldboolean_customfielddate_and_more.py +++ b/src/documents/migrations/1040_customfield_customfieldboolean_customfielddate_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-25 13:49 +# Generated by Django 4.2.5 on 2023-10-28 02:54 import django.db.models.deletion import django.utils.timezone @@ -43,8 +43,8 @@ class Migration(migrations.Migration): ("url", "URL"), ("date", "Date"), ("boolean", "Boolean"), + ("integer", "Integer"), ], - default="string", max_length=50, verbose_name="data type", ), @@ -56,54 +56,6 @@ class Migration(migrations.Migration): "ordering": ("created",), }, ), - migrations.CreateModel( - name="CustomFieldBoolean", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("value", models.BooleanField()), - ( - "parent", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - related_name="boolean", - to="documents.customfieldinstance", - ), - ), - ], - ), - migrations.CreateModel( - name="CustomFieldDate", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("value", models.DateField()), - ( - "parent", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - related_name="date", - to="documents.customfieldinstance", - ), - ), - ], - ), migrations.CreateModel( name="CustomFieldInstance", fields=[ @@ -157,6 +109,78 @@ class Migration(migrations.Migration): "ordering": ("created",), }, ), + migrations.CreateModel( + name="CustomFieldBoolean", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.BooleanField()), + ( + "parent", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + related_name="boolean", + to="documents.customfieldinstance", + ), + ), + ], + ), + migrations.CreateModel( + name="CustomFieldDate", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.DateField()), + ( + "parent", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + related_name="date", + to="documents.customfieldinstance", + ), + ), + ], + ), + migrations.CreateModel( + name="CustomFieldInteger", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.IntegerField()), + ( + "parent", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + related_name="integer", + to="documents.customfieldinstance", + ), + ), + ], + ), migrations.CreateModel( name="CustomFieldShortText", fields=[ diff --git a/src/documents/models.py b/src/documents/models.py index d5423eb41..ae42a09d7 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -895,6 +895,7 @@ class CustomField(models.Model): URL = ("url", _("URL")) DATE = ("date", _("Date")) BOOL = ("boolean"), _("Boolean") + INT = ("integer", _("Integer")) created = models.DateTimeField( _("created"), @@ -908,7 +909,6 @@ class CustomField(models.Model): _("data type"), max_length=50, choices=FieldDataType.choices, - default=FieldDataType.STRING, ) class Meta: @@ -969,6 +969,8 @@ class CustomFieldInstance(ModelWithOwner): return self.date.value elif self.field.data_type == CustomField.FieldDataType.BOOL: return self.boolean.value + elif self.field.data_type == CustomField.FieldDataType.INT: + return self.integer.value raise NotImplementedError(self.field.data_type) @property @@ -984,6 +986,8 @@ class CustomFieldInstance(ModelWithOwner): return CustomFieldDate elif self.field.data_type == CustomField.FieldDataType.BOOL: return CustomFieldBoolean + elif self.field.data_type == CustomField.FieldDataType.INT: + return CustomFieldInteger raise NotImplementedError(self.field.data_type) def to_json(self) -> dict[str, str]: @@ -1077,3 +1081,20 @@ class CustomFieldDate(models.Model): def __str__(self) -> str: return f"{self.value}" + + +class CustomFieldInteger(models.Model): + """ + Data storage for a date custom field + """ + + value = models.IntegerField() + parent = models.OneToOneField( + CustomFieldInstance, + on_delete=models.CASCADE, + related_name="integer", + parent_link=True, + ) + + def __str__(self) -> str: + return f"{self.value}" diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index de93eb0a0..3bfb4b3c4 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -396,6 +396,40 @@ class StoragePathField(serializers.PrimaryKeyRelatedField): return StoragePath.objects.all() +class CustomFieldSerializer(serializers.ModelSerializer): + class Meta: + model = CustomField + fields = [ + "id", + "name", + "data_type", + ] + + +class CustomFieldInstanceSerializer(serializers.ModelSerializer): + parent = CustomFieldSerializer() + value = SerializerMethodField() + + def get_value(self, obj: CustomFieldInstance): + return obj.value + + def create(self, validated_data): + parent_data = validated_data.pop("parent") + parent = CustomField.objects.get(id=parent_data["id"]) + instance = CustomFieldInstance.objects.create(parent=parent) + return instance + + def update(self, instance: CustomFieldInstance): + return instance + + class Meta: + model = CustomFieldInstance + fields = [ + "parent", + "value", + ] + + class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer): correspondent = CorrespondentField(allow_null=True) tags = TagsField(many=True) @@ -406,6 +440,8 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer): archived_file_name = SerializerMethodField() created_date = serializers.DateField(required=False) + custom_fields = CustomFieldInstanceSerializer(many=True, allow_null=True) + owner = serializers.PrimaryKeyRelatedField( queryset=User.objects.all(), required=False, @@ -468,6 +504,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer): "user_can_change", "set_permissions", "notes", + "custom_fields", ) @@ -1092,13 +1129,3 @@ class ConsumptionTemplateSerializer(serializers.ModelSerializer): ) return attrs - - -class CustomFieldSerializer(serializers.ModelSerializer): - class Meta: - model = CustomField - - -class CustomFieldInstanceSerializer(serializers.ModelSerializer): - class Meta: - model = CustomFieldInstance diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py new file mode 100644 index 000000000..f150fafae --- /dev/null +++ b/src/documents/tests/test_api_custom_fields.py @@ -0,0 +1,82 @@ +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import CustomField +from documents.models import Document +from documents.tests.utils import DirectoriesMixin + + +class TestCustomField(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/custom_fields/" + + def setUp(self): + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=self.user) + return super().setUp() + + def test_create_custom_field(self): + """ + GIVEN: + - Each of the supported data types is created + WHEN: + - API request to create custom metadata is made + THEN: + - the field is created + - the field returns the correct fields + """ + for field_type, name in [ + ("string", "Custom Text"), + ("url", "Wikipedia Link"), + ("date", "Invoiced Date"), + ("integer", "Invoice #"), + ]: + resp = self.client.post( + self.ENDPOINT, + data={ + "data_type": field_type, + "name": name, + }, + ) + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + + data = resp.json() + + self.assertEqual(len(data), 3) + self.assertEqual(data["name"], name) + self.assertEqual(data["data_type"], field_type) + + def test_create_custom_field_instance(self): + doc = Document.objects.create( + title="WOW", + content="the content", + checksum="123", + mime_type="application/pdf", + ) + custom_field = CustomField.objects.create( + name="Test Custom Field", + data_type=CustomField.FieldDataType.STRING, + ) + + resp = self.client.patch( + f"/api/documents/{doc.id}/", + data={ + "custom_fields": [ + { + "parent": { + "id": custom_field.id, + }, + "value": "test value", + }, + ], + }, + format="json", + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + doc.refresh_from_db() + from pprint import pprint + + for item in doc.custom_fields.all(): + pprint(item) + self.assertFalse(True) diff --git a/src/documents/tests/test_api_custom_metadata.py b/src/documents/tests/test_api_custom_metadata.py deleted file mode 100644 index a867f275c..000000000 --- a/src/documents/tests/test_api_custom_metadata.py +++ /dev/null @@ -1,291 +0,0 @@ -from datetime import timedelta -from unittest import mock -from unittest.mock import MagicMock - -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.utils import timezone -from guardian.shortcuts import assign_perm -from rest_framework import status -from rest_framework.test import APITestCase - -from documents.models import CustomMetadata -from documents.models import Document -from documents.tests.utils import DirectoriesMixin - - -class TestCustomMetadata(DirectoriesMixin, APITestCase): - def setUp(self): - self.user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=self.user) - return super().setUp() - - @staticmethod - def create_json_no_date(metadata: CustomMetadata): - """ - Small helper to remove the created datatime from the JSON - It doesn't matter to verify - """ - expected = metadata.to_json() - del expected["created"] - return expected - - def test_get_existing_custom_metadata(self): - """ - GIVEN: - - A document with 2 different metadata attached to it - WHEN: - - API request for document custom metadata is made - THEN: - - Both associated values are returned - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have custom metadata on it! Neat", - ) - - metadata1 = CustomMetadata.objects.create( - data_type=CustomMetadata.DataType.STRING, - name="Invoice Number", - data="#123456", - document=doc, - user=self.user, - ) - - metadata2 = CustomMetadata.objects.create( - data_type=CustomMetadata.DataType.URL, - name="October 20th, 2023 On This Day", - data="https://en.wikipedia.org/wiki/Pope_Pius_XII", - document=doc, - user=self.user, - ) - - all_metadata = [metadata1, metadata2] - - response = self.client.get( - f"/api/documents/{doc.pk}/custom_metadata/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertEqual(len(resp_data), 2) - - for idx, resp_data in enumerate(reversed(resp_data)): - del resp_data["created"] - - self.assertDictEqual( - resp_data, - self.create_json_no_date(all_metadata[idx]), - ) - - def test_create_custom_metadata(self): - """ - GIVEN: - - Existing document - WHEN: - - API request is made to add 2 custom metadata fields - THEN: - - metadata objects are created and associated with document - - Document modified time is updated - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have custom_metadata added", - created=timezone.now() - timedelta(days=1), - ) - # set to yesterday - doc.modified = timezone.now() - timedelta(days=1) - self.assertEqual(doc.modified.day, (timezone.now() - timedelta(days=1)).day) - - resp = self.client.post( - f"/api/documents/{doc.pk}/custom_metadata/", - data={"type": "string", "name": "Custom Field 1", "data": "Custom Data 1"}, - ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - - response = self.client.get( - f"/api/documents/{doc.pk}/custom_metadata/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertEqual(len(resp_data), 1) - - resp_data = resp_data[0] - - self.assertEqual(resp_data["data"], "Custom Data 1") - - doc = Document.objects.get(pk=doc.pk) - # modified was updated to today - self.assertEqual(doc.modified.day, timezone.now().day) - - def test_custom_metadata_view_add_delete_permissions_aware(self): - """ - GIVEN: - - Existing document owned by user2 but with granted view perms for user1 - WHEN: - - API request is made by user1 to add a custom metadata - THEN: - - custom metadata is not created - """ - user1 = User.objects.create_user(username="test1") - user1.user_permissions.add(*Permission.objects.all()) - user1.save() - - user2 = User.objects.create_user(username="test2") - user2.save() - - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have custom_metadata added", - ) - doc.owner = user2 - doc.save() - - self.client.force_authenticate(user1) - - resp = self.client.get( - f"/api/documents/{doc.pk}/custom_metadata/", - format="json", - ) - self.assertEqual( - resp.content, - b"Insufficient permissions to view custom metadata", - ) - self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - - assign_perm("view_document", user1, doc) - - resp = self.client.post( - f"/api/documents/{doc.pk}/custom_metadata/", - data={"type": "string", "name": "Custom Field 1", "data": "Custom Data 1"}, - ) - self.assertEqual( - resp.content, - b"Insufficient permissions to create custom metadata", - ) - self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - - metadata = CustomMetadata.objects.create( - data_type=CustomMetadata.DataType.STRING, - name="Invoice Number", - data="#123456", - document=doc, - user=self.user, - ) - - response = self.client.delete( - f"/api/documents/{doc.pk}/custom_metadata/?id={metadata.pk}", - format="json", - ) - - self.assertEqual( - response.content, - b"Insufficient permissions to delete custom metadata", - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_custom_metadata(self): - """ - GIVEN: - - Existing document, existing custom metadata - WHEN: - - API request is made to delete a custom metadata - THEN: - - custom metadata is deleted, document modified is updated - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have custom metadata!", - created=timezone.now() - timedelta(days=1), - ) - # set to yesterday - doc.modified = timezone.now() - timedelta(days=1) - self.assertEqual(doc.modified.day, (timezone.now() - timedelta(days=1)).day) - - metadata = CustomMetadata.objects.create( - data_type=CustomMetadata.DataType.DATE, - name="Invoice Number", - data="2023-10-20", - document=doc, - user=self.user, - ) - - response = self.client.delete( - f"/api/documents/{doc.pk}/custom_metadata/?id={metadata.pk}", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.assertEqual(len(CustomMetadata.objects.all()), 0) - doc = Document.objects.get(pk=doc.pk) - # modified was updated to today - self.assertEqual(doc.modified.day, timezone.now().day) - - def test_get_custom_metadata_no_doc(self): - """ - GIVEN: - - A request to get custom metadata from a non-existent document - WHEN: - - API request for document custom metadata is made - THEN: - - HTTP status.HTTP_404_NOT_FOUND is returned - """ - response = self.client.get( - "/api/documents/500/custom_metadata/", - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - @mock.patch("documents.views.CustomMetadata.to_json") - def test_get_custom_metadata_failure(self, mocked_to_json: MagicMock): - mocked_to_json.side_effect = Exception("this failed somehow") - - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have custom metadata on it! Neat", - ) - - _ = CustomMetadata.objects.create( - data_type=CustomMetadata.DataType.STRING, - name="Invoice Number", - data="#123456", - document=doc, - user=self.user, - ) - - response = self.client.get( - f"/api/documents/{doc.pk}/custom_metadata/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - - @mock.patch("documents.views.CustomMetadata.from_json") - def test_add_custom_metadata_failure(self, mocked_from_json: MagicMock): - mocked_from_json.side_effect = Exception("this failed somehow else") - - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have custom metadata on it! Neat", - ) - - response = self.client.post( - f"/api/documents/{doc.pk}/custom_metadata/", - data={"type": "string", "name": "Custom Field 1", "data": "Custom Data 1"}, - ) - - self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/src/documents/views.py b/src/documents/views.py index 1d9d21d3f..799cd895b 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -624,100 +624,6 @@ class DocumentViewSet( ] return Response(links) - # @action(methods=["get", "post", "delete"], detail=True) - # def custom_metadata(self, request, pk=None) -> Response: - # def package_custom_metadata(doc: Document): - # return [ - # c.to_json() - # for c in CustomMetadata.objects.filter(document=doc).order_by( - # "-created", - # ) - # ] - - # request.user = request.user - # try: - # doc = Document.objects.get(pk=pk) - # if request.user is not None and not has_perms_owner_aware( - # request.user, - # "view_document", - # doc, - # ): - # return HttpResponseForbidden( - # "Insufficient permissions to view custom metadata", - # ) - # except Document.DoesNotExist: - # raise Http404 - - # if request.method == "GET": - # try: - # return Response(package_custom_metadata(doc)) - # except Exception as e: - # logger.warning(f"An error occurred retrieving custom metadata: {e!s}") - # return HttpResponseServerError( - # { - # "error": ( - # "Error retrieving custom metadata," - # " check logs for more detail." - # ), - # }, - # ) - # elif request.method == "POST": - # try: - # if request.user is not None and not has_perms_owner_aware( - # request.user, - # "change_document", - # doc, - # ): - # return HttpResponseForbidden( - # "Insufficient permissions to create custom metadata", - # ) - - # CustomMetadata.from_json(doc, request.user, request.data) - - # doc.modified = timezone.now() - # doc.save() - - # from documents import index - - # index.add_or_update_document(self.get_object()) - - # return Response(package_custom_metadata(doc)) - # except Exception as e: - # logger.warning(f"An error occurred saving custom metadata: {e!s}") - # return HttpResponseServerError( - # { - # "error": ( - # "Error saving custom metadata, " - # "check logs for more detail." - # ), - # }, - # ) - # elif request.method == "DELETE": - # if request.user is not None and not has_perms_owner_aware( - # request.user, - # "change_document", - # doc, - # ): - # return HttpResponseForbidden( - # "Insufficient permissions to delete custom metadata", - # ) - - # metadata = CustomMetadata.objects.get(id=int(request.GET.get("id"))) - # metadata.delete() - - # doc.modified = timezone.now() - # doc.save() - - # from documents import index - - # index.add_or_update_document(self.get_object()) - - # return Response(package_custom_metadata(doc)) - - # return Response( - # {"error": "unreachable error was reached for custom metadata"}, - # ) # pragma: no cover - class SearchResultSerializer(DocumentSerializer, PassUserMixin): def to_representation(self, instance): @@ -1441,7 +1347,7 @@ class ConsumptionTemplateViewSet(ModelViewSet): model = ConsumptionTemplate - queryset = ConsumptionTemplate.objects.all().order_by("order") + queryset = ConsumptionTemplate.objects.all().order_by("name") class CustomFieldViewSet(ModelViewSet):