+++ /dev/null
-import datetime
-import io
-import json
-import os
-import shutil
-import tempfile
-import urllib.request
-import uuid
-import zipfile
-import zoneinfo
-from datetime import timedelta
-from pathlib import Path
-from unittest import mock
-from unittest.mock import MagicMock
-
-import celery
-from dateutil import parser
-from django.conf import settings
-from django.contrib.auth.models import Group
-from django.contrib.auth.models import Permission
-from django.contrib.auth.models import User
-from django.test import override_settings
-from django.utils import timezone
-from guardian.shortcuts import assign_perm
-from guardian.shortcuts import get_perms
-from guardian.shortcuts import get_users_with_perms
-from rest_framework import status
-from rest_framework.test import APITestCase
-
-from documents import bulk_edit
-from documents.data_models import DocumentSource
-from documents.models import ConsumptionTemplate
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import Note
-from documents.models import PaperlessTask
-from documents.models import SavedView
-from documents.models import ShareLink
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.tests.utils import DirectoriesMixin
-from documents.tests.utils import DocumentConsumeDelayMixin
-from paperless import version
-from paperless_mail.models import MailAccount
-from paperless_mail.models import MailRule
-
-
-class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
- def setUp(self):
- super().setUp()
-
- self.user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=self.user)
-
- def testDocuments(self):
- response = self.client.get("/api/documents/").data
-
- self.assertEqual(response["count"], 0)
-
- c = Correspondent.objects.create(name="c", pk=41)
- dt = DocumentType.objects.create(name="dt", pk=63)
- tag = Tag.objects.create(name="t", pk=85)
-
- doc = Document.objects.create(
- title="WOW",
- content="the content",
- correspondent=c,
- document_type=dt,
- checksum="123",
- mime_type="application/pdf",
- )
-
- doc.tags.add(tag)
-
- response = self.client.get("/api/documents/", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 1)
-
- returned_doc = response.data["results"][0]
- self.assertEqual(returned_doc["id"], doc.id)
- self.assertEqual(returned_doc["title"], doc.title)
- self.assertEqual(returned_doc["correspondent"], c.id)
- self.assertEqual(returned_doc["document_type"], dt.id)
- self.assertListEqual(returned_doc["tags"], [tag.id])
-
- c2 = Correspondent.objects.create(name="c2")
-
- returned_doc["correspondent"] = c2.pk
- returned_doc["title"] = "the new title"
-
- response = self.client.put(
- f"/api/documents/{doc.pk}/",
- returned_doc,
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- doc_after_save = Document.objects.get(id=doc.id)
-
- self.assertEqual(doc_after_save.correspondent, c2)
- self.assertEqual(doc_after_save.title, "the new title")
-
- self.client.delete(f"/api/documents/{doc_after_save.pk}/")
-
- self.assertEqual(len(Document.objects.all()), 0)
-
- def test_document_fields(self):
- c = Correspondent.objects.create(name="c", pk=41)
- dt = DocumentType.objects.create(name="dt", pk=63)
- Tag.objects.create(name="t", pk=85)
- storage_path = StoragePath.objects.create(name="sp", pk=77, path="p")
- Document.objects.create(
- title="WOW",
- content="the content",
- correspondent=c,
- document_type=dt,
- checksum="123",
- mime_type="application/pdf",
- storage_path=storage_path,
- )
-
- response = self.client.get("/api/documents/", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results_full = response.data["results"]
- self.assertIn("content", results_full[0])
- self.assertIn("id", results_full[0])
-
- response = self.client.get("/api/documents/?fields=id", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertFalse("content" in results[0])
- self.assertIn("id", results[0])
- self.assertEqual(len(results[0]), 1)
-
- response = self.client.get("/api/documents/?fields=content", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertIn("content", results[0])
- self.assertFalse("id" in results[0])
- self.assertEqual(len(results[0]), 1)
-
- response = self.client.get("/api/documents/?fields=id,content", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertIn("content", results[0])
- self.assertIn("id", results[0])
- self.assertEqual(len(results[0]), 2)
-
- response = self.client.get(
- "/api/documents/?fields=id,conteasdnt",
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertFalse("content" in results[0])
- self.assertIn("id", results[0])
- self.assertEqual(len(results[0]), 1)
-
- response = self.client.get("/api/documents/?fields=", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results_full[0]), len(results[0]))
-
- response = self.client.get("/api/documents/?fields=dgfhs", format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results[0]), 0)
-
- def test_document_actions(self):
- _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
-
- content = b"This is a test"
- content_thumbnail = b"thumbnail content"
-
- with open(filename, "wb") as f:
- f.write(content)
-
- doc = Document.objects.create(
- title="none",
- filename=os.path.basename(filename),
- mime_type="application/pdf",
- )
-
- with open(
- os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"),
- "wb",
- ) as f:
- f.write(content_thumbnail)
-
- response = self.client.get(f"/api/documents/{doc.pk}/download/")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content)
-
- response = self.client.get(f"/api/documents/{doc.pk}/preview/")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content)
-
- response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content_thumbnail)
-
- def test_document_actions_with_perms(self):
- """
- GIVEN:
- - Document with owner and without granted permissions
- - User is then granted permissions
- WHEN:
- - User tries to load preview, thumbnail
- THEN:
- - Initially, HTTP 403 Forbidden
- - With permissions, HTTP 200 OK
- """
- _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
-
- content = b"This is a test"
- content_thumbnail = b"thumbnail content"
-
- with open(filename, "wb") as f:
- f.write(content)
-
- user1 = User.objects.create_user(username="test1")
- user2 = User.objects.create_user(username="test2")
- user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
- user2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
-
- self.client.force_authenticate(user2)
-
- doc = Document.objects.create(
- title="none",
- filename=os.path.basename(filename),
- mime_type="application/pdf",
- owner=user1,
- )
-
- with open(
- os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"),
- "wb",
- ) as f:
- f.write(content_thumbnail)
-
- response = self.client.get(f"/api/documents/{doc.pk}/download/")
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- response = self.client.get(f"/api/documents/{doc.pk}/preview/")
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- assign_perm("view_document", user2, doc)
-
- response = self.client.get(f"/api/documents/{doc.pk}/download/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- response = self.client.get(f"/api/documents/{doc.pk}/preview/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- @override_settings(FILENAME_FORMAT="")
- def test_download_with_archive(self):
- content = b"This is a test"
- content_archive = b"This is the same test but archived"
-
- doc = Document.objects.create(
- title="none",
- filename="my_document.pdf",
- archive_filename="archived.pdf",
- mime_type="application/pdf",
- )
-
- with open(doc.source_path, "wb") as f:
- f.write(content)
-
- with open(doc.archive_path, "wb") as f:
- f.write(content_archive)
-
- response = self.client.get(f"/api/documents/{doc.pk}/download/")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content_archive)
-
- response = self.client.get(
- f"/api/documents/{doc.pk}/download/?original=true",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content)
-
- response = self.client.get(f"/api/documents/{doc.pk}/preview/")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content_archive)
-
- response = self.client.get(
- f"/api/documents/{doc.pk}/preview/?original=true",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.content, content)
-
- def test_document_actions_not_existing_file(self):
- doc = Document.objects.create(
- title="none",
- filename=os.path.basename("asd"),
- mime_type="application/pdf",
- )
-
- response = self.client.get(f"/api/documents/{doc.pk}/download/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- response = self.client.get(f"/api/documents/{doc.pk}/preview/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_document_filters(self):
- doc1 = Document.objects.create(
- title="none1",
- checksum="A",
- mime_type="application/pdf",
- )
- doc2 = Document.objects.create(
- title="none2",
- checksum="B",
- mime_type="application/pdf",
- )
- doc3 = Document.objects.create(
- title="none3",
- checksum="C",
- mime_type="application/pdf",
- )
-
- tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
- tag_2 = Tag.objects.create(name="t2")
- tag_3 = Tag.objects.create(name="t3")
-
- cf1 = CustomField.objects.create(
- name="stringfield",
- data_type=CustomField.FieldDataType.STRING,
- )
- cf2 = CustomField.objects.create(
- name="numberfield",
- data_type=CustomField.FieldDataType.INT,
- )
-
- doc1.tags.add(tag_inbox)
- doc2.tags.add(tag_2)
- doc3.tags.add(tag_2)
- doc3.tags.add(tag_3)
-
- cf1_d1 = CustomFieldInstance.objects.create(
- document=doc1,
- field=cf1,
- value_text="foobard1",
- )
- CustomFieldInstance.objects.create(
- document=doc1,
- field=cf2,
- value_int=999,
- )
- cf1_d3 = CustomFieldInstance.objects.create(
- document=doc3,
- field=cf1,
- value_text="foobard3",
- )
-
- response = self.client.get("/api/documents/?is_in_inbox=true")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc1.id)
-
- response = self.client.get("/api/documents/?is_in_inbox=false")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
- self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id])
-
- response = self.client.get(
- f"/api/documents/?tags__id__in={tag_inbox.id},{tag_3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
- self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc3.id])
-
- response = self.client.get(
- f"/api/documents/?tags__id__in={tag_2.id},{tag_3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
- self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id])
-
- response = self.client.get(
- f"/api/documents/?tags__id__all={tag_2.id},{tag_3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc3.id)
-
- response = self.client.get(
- f"/api/documents/?tags__id__all={tag_inbox.id},{tag_3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 0)
-
- response = self.client.get(
- f"/api/documents/?tags__id__all={tag_inbox.id}a{tag_3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 3)
-
- response = self.client.get(f"/api/documents/?tags__id__none={tag_3.id}")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
- self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc2.id])
-
- response = self.client.get(
- f"/api/documents/?tags__id__none={tag_3.id},{tag_2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc1.id)
-
- response = self.client.get(
- f"/api/documents/?tags__id__none={tag_2.id},{tag_inbox.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 0)
-
- response = self.client.get(
- f"/api/documents/?id__in={doc1.id},{doc2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
-
- response = self.client.get(
- f"/api/documents/?id__range={doc1.id},{doc3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 3)
-
- response = self.client.get(
- f"/api/documents/?id={doc2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
-
- # custom field name
- response = self.client.get(
- f"/api/documents/?custom_fields__icontains={cf1.name}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
-
- # custom field value
- response = self.client.get(
- f"/api/documents/?custom_fields__icontains={cf1_d1.value}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc1.id)
-
- response = self.client.get(
- f"/api/documents/?custom_fields__icontains={cf1_d3.value}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc3.id)
-
- def test_document_checksum_filter(self):
- Document.objects.create(
- title="none1",
- checksum="A",
- mime_type="application/pdf",
- )
- doc2 = Document.objects.create(
- title="none2",
- checksum="B",
- mime_type="application/pdf",
- )
- Document.objects.create(
- title="none3",
- checksum="C",
- mime_type="application/pdf",
- )
-
- response = self.client.get("/api/documents/?checksum__iexact=B")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc2.id)
-
- response = self.client.get("/api/documents/?checksum__iexact=X")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 0)
-
- def test_document_original_filename_filter(self):
- doc1 = Document.objects.create(
- title="none1",
- checksum="A",
- mime_type="application/pdf",
- original_filename="docA.pdf",
- )
- doc2 = Document.objects.create(
- title="none2",
- checksum="B",
- mime_type="application/pdf",
- original_filename="docB.pdf",
- )
- doc3 = Document.objects.create(
- title="none3",
- checksum="C",
- mime_type="application/pdf",
- original_filename="docC.pdf",
- )
-
- response = self.client.get("/api/documents/?original_filename__iexact=DOCa.pdf")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertEqual(results[0]["id"], doc1.id)
-
- response = self.client.get("/api/documents/?original_filename__iexact=docx.pdf")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 0)
-
- response = self.client.get("/api/documents/?original_filename__istartswith=dOc")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 3)
- self.assertCountEqual(
- [results[0]["id"], results[1]["id"], results[2]["id"]],
- [doc1.id, doc2.id, doc3.id],
- )
-
- def test_documents_title_content_filter(self):
- doc1 = Document.objects.create(
- title="title A",
- content="content A",
- checksum="A",
- mime_type="application/pdf",
- )
- doc2 = Document.objects.create(
- title="title B",
- content="content A",
- checksum="B",
- mime_type="application/pdf",
- )
- doc3 = Document.objects.create(
- title="title A",
- content="content B",
- checksum="C",
- mime_type="application/pdf",
- )
- doc4 = Document.objects.create(
- title="title B",
- content="content B",
- checksum="D",
- mime_type="application/pdf",
- )
-
- response = self.client.get("/api/documents/?title_content=A")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 3)
- self.assertCountEqual(
- [results[0]["id"], results[1]["id"], results[2]["id"]],
- [doc1.id, doc2.id, doc3.id],
- )
-
- response = self.client.get("/api/documents/?title_content=B")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 3)
- self.assertCountEqual(
- [results[0]["id"], results[1]["id"], results[2]["id"]],
- [doc2.id, doc3.id, doc4.id],
- )
-
- response = self.client.get("/api/documents/?title_content=X")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 0)
-
- def test_document_owner_filters(self):
- """
- GIVEN:
- - Documents with owners, with and without granted permissions
- WHEN:
- - User filters by owner
- THEN:
- - Owner filters work correctly but still respect permissions
- """
- u1 = User.objects.create_user("user1")
- u2 = User.objects.create_user("user2")
- u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
- u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
-
- u1_doc1 = Document.objects.create(
- title="none1",
- checksum="A",
- mime_type="application/pdf",
- owner=u1,
- )
- Document.objects.create(
- title="none2",
- checksum="B",
- mime_type="application/pdf",
- owner=u2,
- )
- u0_doc1 = Document.objects.create(
- title="none3",
- checksum="C",
- mime_type="application/pdf",
- )
- u1_doc2 = Document.objects.create(
- title="none4",
- checksum="D",
- mime_type="application/pdf",
- owner=u1,
- )
- u2_doc2 = Document.objects.create(
- title="none5",
- checksum="E",
- mime_type="application/pdf",
- owner=u2,
- )
-
- self.client.force_authenticate(user=u1)
- assign_perm("view_document", u1, u2_doc2)
-
- # Will not show any u1 docs or u2_doc1 which isn't shared
- response = self.client.get(f"/api/documents/?owner__id__none={u1.id}")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
- self.assertCountEqual(
- [results[0]["id"], results[1]["id"]],
- [u0_doc1.id, u2_doc2.id],
- )
-
- # Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared
- response = self.client.get(
- f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertCountEqual([results[0]["id"]], [u2_doc2.id])
-
- # Will not show any u1 docs, u2_doc2 which is shared but has owner
- response = self.client.get(
- f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertCountEqual([results[0]["id"]], [u0_doc1.id])
-
- # Will not show any u1 docs or u2_doc1 which is not shared
- response = self.client.get(f"/api/documents/?owner__id={u2.id}")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
- self.assertCountEqual([results[0]["id"]], [u2_doc2.id])
-
- # Will not show u2_doc1 which is not shared
- response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 3)
- self.assertCountEqual(
- [results[0]["id"], results[1]["id"], results[2]["id"]],
- [u1_doc1.id, u1_doc2.id, u2_doc2.id],
- )
-
- def test_pagination_all(self):
- """
- GIVEN:
- - A set of 50 documents
- WHEN:
- - API request for document filtering
- THEN:
- - Results are paginated (25 items) and response["all"] returns all ids (50 items)
- """
- t = Tag.objects.create(name="tag")
- docs = []
- for i in range(50):
- d = Document.objects.create(checksum=i, content=f"test{i}")
- d.tags.add(t)
- docs.append(d)
-
- response = self.client.get(
- f"/api/documents/?tags__id__in={t.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 25)
- self.assertEqual(len(response.data["all"]), 50)
- self.assertCountEqual(response.data["all"], [d.id for d in docs])
-
- def test_statistics(self):
- doc1 = Document.objects.create(
- title="none1",
- checksum="A",
- mime_type="application/pdf",
- content="abc",
- )
- Document.objects.create(
- title="none2",
- checksum="B",
- mime_type="application/pdf",
- content="123",
- )
- Document.objects.create(
- title="none3",
- checksum="C",
- mime_type="text/plain",
- content="hello",
- )
-
- tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
- Tag.objects.create(name="t2")
- Tag.objects.create(name="t3")
- Correspondent.objects.create(name="c1")
- Correspondent.objects.create(name="c2")
- DocumentType.objects.create(name="dt1")
- StoragePath.objects.create(name="sp1")
- StoragePath.objects.create(name="sp2")
-
- doc1.tags.add(tag_inbox)
-
- response = self.client.get("/api/statistics/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["documents_total"], 3)
- self.assertEqual(response.data["documents_inbox"], 1)
- self.assertEqual(response.data["inbox_tag"], tag_inbox.pk)
- self.assertEqual(
- response.data["document_file_type_counts"][0]["mime_type_count"],
- 2,
- )
- self.assertEqual(
- response.data["document_file_type_counts"][1]["mime_type_count"],
- 1,
- )
- self.assertEqual(response.data["character_count"], 11)
- self.assertEqual(response.data["tag_count"], 3)
- self.assertEqual(response.data["correspondent_count"], 2)
- self.assertEqual(response.data["document_type_count"], 1)
- self.assertEqual(response.data["storage_path_count"], 2)
-
- def test_statistics_no_inbox_tag(self):
- Document.objects.create(title="none1", checksum="A")
-
- response = self.client.get("/api/statistics/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["documents_inbox"], None)
- self.assertEqual(response.data["inbox_tag"], None)
-
- def test_upload(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f},
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- input_doc, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(input_doc.original_file.name, "simple.pdf")
- self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents)
- self.assertIsNone(overrides.title)
- self.assertIsNone(overrides.correspondent_id)
- self.assertIsNone(overrides.document_type_id)
- self.assertIsNone(overrides.tag_ids)
-
- def test_upload_empty_metadata(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "title": "", "correspondent": "", "document_type": ""},
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- input_doc, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(input_doc.original_file.name, "simple.pdf")
- self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents)
- self.assertIsNone(overrides.title)
- self.assertIsNone(overrides.correspondent_id)
- self.assertIsNone(overrides.document_type_id)
- self.assertIsNone(overrides.tag_ids)
-
- def test_upload_invalid_form(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"documenst": f},
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.consume_file_mock.assert_not_called()
-
- def test_upload_invalid_file(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.zip"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f},
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.consume_file_mock.assert_not_called()
-
- def test_upload_with_title(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "title": "my custom title"},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- _, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(overrides.title, "my custom title")
- self.assertIsNone(overrides.correspondent_id)
- self.assertIsNone(overrides.document_type_id)
- self.assertIsNone(overrides.tag_ids)
-
- def test_upload_with_correspondent(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- c = Correspondent.objects.create(name="test-corres")
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "correspondent": c.id},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- _, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(overrides.correspondent_id, c.id)
- self.assertIsNone(overrides.title)
- self.assertIsNone(overrides.document_type_id)
- self.assertIsNone(overrides.tag_ids)
-
- def test_upload_with_invalid_correspondent(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "correspondent": 3456},
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- self.consume_file_mock.assert_not_called()
-
- def test_upload_with_document_type(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- dt = DocumentType.objects.create(name="invoice")
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "document_type": dt.id},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- _, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(overrides.document_type_id, dt.id)
- self.assertIsNone(overrides.correspondent_id)
- self.assertIsNone(overrides.title)
- self.assertIsNone(overrides.tag_ids)
-
- def test_upload_with_invalid_document_type(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "document_type": 34578},
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- self.consume_file_mock.assert_not_called()
-
- def test_upload_with_tags(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- t1 = Tag.objects.create(name="tag1")
- t2 = Tag.objects.create(name="tag2")
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "tags": [t2.id, t1.id]},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- _, overrides = self.get_last_consume_delay_call_args()
-
- self.assertCountEqual(overrides.tag_ids, [t1.id, t2.id])
- self.assertIsNone(overrides.document_type_id)
- self.assertIsNone(overrides.correspondent_id)
- self.assertIsNone(overrides.title)
-
- def test_upload_with_invalid_tags(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- t1 = Tag.objects.create(name="tag1")
- t2 = Tag.objects.create(name="tag2")
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "tags": [t2.id, t1.id, 734563]},
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- self.consume_file_mock.assert_not_called()
-
- def test_upload_with_created(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- created = datetime.datetime(
- 2022,
- 5,
- 12,
- 0,
- 0,
- 0,
- 0,
- tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
- )
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "created": created},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- _, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(overrides.created, created)
-
- def test_upload_with_asn(self):
- self.consume_file_mock.return_value = celery.result.AsyncResult(
- id=str(uuid.uuid4()),
- )
-
- with open(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- "rb",
- ) as f:
- response = self.client.post(
- "/api/documents/post_document/",
- {"document": f, "archive_serial_number": 500},
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.consume_file_mock.assert_called_once()
-
- input_doc, overrides = self.get_last_consume_delay_call_args()
-
- self.assertEqual(input_doc.original_file.name, "simple.pdf")
- self.assertEqual(overrides.filename, "simple.pdf")
- self.assertIsNone(overrides.correspondent_id)
- self.assertIsNone(overrides.document_type_id)
- self.assertIsNone(overrides.tag_ids)
- self.assertEqual(500, overrides.asn)
-
- def test_get_metadata(self):
- doc = Document.objects.create(
- title="test",
- filename="file.pdf",
- mime_type="image/png",
- archive_checksum="A",
- archive_filename="archive.pdf",
- )
-
- source_file = os.path.join(
- os.path.dirname(__file__),
- "samples",
- "documents",
- "thumbnails",
- "0000001.webp",
- )
- archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
-
- shutil.copy(source_file, doc.source_path)
- shutil.copy(archive_file, doc.archive_path)
-
- response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- meta = response.data
-
- self.assertEqual(meta["original_mime_type"], "image/png")
- self.assertTrue(meta["has_archive_version"])
- self.assertEqual(len(meta["original_metadata"]), 0)
- self.assertGreater(len(meta["archive_metadata"]), 0)
- self.assertEqual(meta["media_filename"], "file.pdf")
- self.assertEqual(meta["archive_media_filename"], "archive.pdf")
- self.assertEqual(meta["original_size"], os.stat(source_file).st_size)
- self.assertEqual(meta["archive_size"], os.stat(archive_file).st_size)
-
- def test_get_metadata_invalid_doc(self):
- response = self.client.get("/api/documents/34576/metadata/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_get_metadata_no_archive(self):
- doc = Document.objects.create(
- title="test",
- filename="file.pdf",
- mime_type="application/pdf",
- )
-
- shutil.copy(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- doc.source_path,
- )
-
- response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- meta = response.data
-
- self.assertEqual(meta["original_mime_type"], "application/pdf")
- self.assertFalse(meta["has_archive_version"])
- self.assertGreater(len(meta["original_metadata"]), 0)
- self.assertIsNone(meta["archive_metadata"])
- self.assertIsNone(meta["archive_media_filename"])
-
- def test_get_metadata_missing_files(self):
- doc = Document.objects.create(
- title="test",
- filename="file.pdf",
- mime_type="application/pdf",
- archive_filename="file.pdf",
- archive_checksum="B",
- checksum="A",
- )
-
- response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- meta = response.data
-
- self.assertTrue(meta["has_archive_version"])
- self.assertIsNone(meta["original_metadata"])
- self.assertIsNone(meta["original_size"])
- self.assertIsNone(meta["archive_metadata"])
- self.assertIsNone(meta["archive_size"])
-
- def test_get_empty_suggestions(self):
- doc = Document.objects.create(title="test", mime_type="application/pdf")
-
- response = self.client.get(f"/api/documents/{doc.pk}/suggestions/")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(
- response.data,
- {
- "correspondents": [],
- "tags": [],
- "document_types": [],
- "storage_paths": [],
- "dates": [],
- },
- )
-
- def test_get_suggestions_invalid_doc(self):
- response = self.client.get("/api/documents/34676/suggestions/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- @mock.patch("documents.views.match_storage_paths")
- @mock.patch("documents.views.match_document_types")
- @mock.patch("documents.views.match_tags")
- @mock.patch("documents.views.match_correspondents")
- @override_settings(NUMBER_OF_SUGGESTED_DATES=10)
- def test_get_suggestions(
- self,
- match_correspondents,
- match_tags,
- match_document_types,
- match_storage_paths,
- ):
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is an invoice from 12.04.2022!",
- )
-
- match_correspondents.return_value = [Correspondent(id=88), Correspondent(id=2)]
- match_tags.return_value = [Tag(id=56), Tag(id=123)]
- match_document_types.return_value = [DocumentType(id=23)]
- match_storage_paths.return_value = [StoragePath(id=99), StoragePath(id=77)]
-
- response = self.client.get(f"/api/documents/{doc.pk}/suggestions/")
- self.assertEqual(
- response.data,
- {
- "correspondents": [88, 2],
- "tags": [56, 123],
- "document_types": [23],
- "storage_paths": [99, 77],
- "dates": ["2022-04-12"],
- },
- )
-
- @mock.patch("documents.parsers.parse_date_generator")
- @override_settings(NUMBER_OF_SUGGESTED_DATES=0)
- def test_get_suggestions_dates_disabled(
- self,
- parse_date_generator,
- ):
- """
- GIVEN:
- - NUMBER_OF_SUGGESTED_DATES = 0 (disables feature)
- WHEN:
- - API reuqest for document suggestions
- THEN:
- - Dont check for suggested dates at all
- """
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is an invoice from 12.04.2022!",
- )
-
- self.client.get(f"/api/documents/{doc.pk}/suggestions/")
- self.assertFalse(parse_date_generator.called)
-
- def test_saved_views(self):
- u1 = User.objects.create_superuser("user1")
- u2 = User.objects.create_superuser("user2")
-
- v1 = SavedView.objects.create(
- owner=u1,
- name="test1",
- sort_field="",
- show_on_dashboard=False,
- show_in_sidebar=False,
- )
- SavedView.objects.create(
- owner=u2,
- name="test2",
- sort_field="",
- show_on_dashboard=False,
- show_in_sidebar=False,
- )
- SavedView.objects.create(
- owner=u2,
- name="test3",
- sort_field="",
- show_on_dashboard=False,
- show_in_sidebar=False,
- )
-
- response = self.client.get("/api/saved_views/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 0)
-
- self.assertEqual(
- self.client.get(f"/api/saved_views/{v1.id}/").status_code,
- status.HTTP_404_NOT_FOUND,
- )
-
- self.client.force_authenticate(user=u1)
-
- response = self.client.get("/api/saved_views/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 1)
-
- self.assertEqual(
- self.client.get(f"/api/saved_views/{v1.id}/").status_code,
- status.HTTP_200_OK,
- )
-
- self.client.force_authenticate(user=u2)
-
- response = self.client.get("/api/saved_views/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 2)
-
- self.assertEqual(
- self.client.get(f"/api/saved_views/{v1.id}/").status_code,
- status.HTTP_404_NOT_FOUND,
- )
-
- def test_create_update_patch(self):
- User.objects.create_user("user1")
-
- view = {
- "name": "test",
- "show_on_dashboard": True,
- "show_in_sidebar": True,
- "sort_field": "created2",
- "filter_rules": [{"rule_type": 4, "value": "test"}],
- }
-
- response = self.client.post("/api/saved_views/", view, format="json")
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- v1 = SavedView.objects.get(name="test")
- self.assertEqual(v1.sort_field, "created2")
- self.assertEqual(v1.filter_rules.count(), 1)
- self.assertEqual(v1.owner, self.user)
-
- response = self.client.patch(
- f"/api/saved_views/{v1.id}/",
- {"show_in_sidebar": False},
- format="json",
- )
-
- v1 = SavedView.objects.get(id=v1.id)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertFalse(v1.show_in_sidebar)
- self.assertEqual(v1.filter_rules.count(), 1)
-
- view["filter_rules"] = [{"rule_type": 12, "value": "secret"}]
-
- response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- v1 = SavedView.objects.get(id=v1.id)
- self.assertEqual(v1.filter_rules.count(), 1)
- self.assertEqual(v1.filter_rules.first().value, "secret")
-
- view["filter_rules"] = []
-
- response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- v1 = SavedView.objects.get(id=v1.id)
- self.assertEqual(v1.filter_rules.count(), 0)
-
- def test_get_logs(self):
- log_data = "test\ntest2\n"
- with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f:
- f.write(log_data)
- with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
- f.write(log_data)
- response = self.client.get("/api/logs/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertCountEqual(response.data, ["mail", "paperless"])
-
- def test_get_logs_only_when_exist(self):
- log_data = "test\ntest2\n"
- with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
- f.write(log_data)
- response = self.client.get("/api/logs/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertCountEqual(response.data, ["paperless"])
-
- def test_get_invalid_log(self):
- response = self.client.get("/api/logs/bogus_log/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- @override_settings(LOGGING_DIR="bogus_dir")
- def test_get_nonexistent_log(self):
- response = self.client.get("/api/logs/paperless/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_get_log(self):
- log_data = "test\ntest2\n"
- with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
- f.write(log_data)
- response = self.client.get("/api/logs/paperless/")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertListEqual(response.data, ["test", "test2"])
-
- def test_invalid_regex_other_algorithm(self):
- for endpoint in ["correspondents", "tags", "document_types"]:
- response = self.client.post(
- f"/api/{endpoint}/",
- {
- "name": "test",
- "matching_algorithm": MatchingModel.MATCH_ANY,
- "match": "[",
- },
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint)
-
- def test_invalid_regex(self):
- for endpoint in ["correspondents", "tags", "document_types"]:
- response = self.client.post(
- f"/api/{endpoint}/",
- {
- "name": "test",
- "matching_algorithm": MatchingModel.MATCH_REGEX,
- "match": "[",
- },
- format="json",
- )
- self.assertEqual(
- response.status_code,
- status.HTTP_400_BAD_REQUEST,
- endpoint,
- )
-
- def test_valid_regex(self):
- for endpoint in ["correspondents", "tags", "document_types"]:
- response = self.client.post(
- f"/api/{endpoint}/",
- {
- "name": "test",
- "matching_algorithm": MatchingModel.MATCH_REGEX,
- "match": "[0-9]",
- },
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint)
-
- def test_regex_no_algorithm(self):
- for endpoint in ["correspondents", "tags", "document_types"]:
- response = self.client.post(
- f"/api/{endpoint}/",
- {"name": "test", "match": "[0-9]"},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint)
-
- def test_tag_color_default(self):
- response = self.client.post("/api/tags/", {"name": "tag"}, format="json")
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#a6cee3")
- self.assertEqual(
- self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[
- "colour"
- ],
- 1,
- )
-
- def test_tag_color(self):
- response = self.client.post(
- "/api/tags/",
- {"name": "tag", "colour": 3},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#b2df8a")
- self.assertEqual(
- self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[
- "colour"
- ],
- 3,
- )
-
- def test_tag_color_invalid(self):
- response = self.client.post(
- "/api/tags/",
- {"name": "tag", "colour": 34},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_tag_color_custom(self):
- tag = Tag.objects.create(name="test", color="#abcdef")
- self.assertEqual(
- self.client.get(f"/api/tags/{tag.id}/", format="json").data["colour"],
- 1,
- )
-
- def test_get_existing_notes(self):
- """
- GIVEN:
- - A document with a single note
- WHEN:
- - API reuqest for document notes is made
- THEN:
- - The associated note is returned
- """
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is a document which will have notes!",
- )
- note = Note.objects.create(
- note="This is a note.",
- document=doc,
- user=self.user,
- )
-
- response = self.client.get(
- f"/api/documents/{doc.pk}/notes/",
- 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]
- del resp_data["created"]
-
- self.assertDictEqual(
- resp_data,
- {
- "id": note.id,
- "note": note.note,
- "user": {
- "id": note.user.id,
- "username": note.user.username,
- "first_name": note.user.first_name,
- "last_name": note.user.last_name,
- },
- },
- )
-
- def test_create_note(self):
- """
- GIVEN:
- - Existing document
- WHEN:
- - API request is made to add a note
- THEN:
- - note is created and associated with document, modified time is updated
- """
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is a document which will have notes 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}/notes/",
- data={"note": "this is a posted note"},
- )
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
- response = self.client.get(
- f"/api/documents/{doc.pk}/notes/",
- 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["note"], "this is a posted note")
-
- doc = Document.objects.get(pk=doc.pk)
- # modified was updated to today
- self.assertEqual(doc.modified.day, timezone.now().day)
-
- def test_notes_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 note or delete
- THEN:
- - Notes are neither created nor deleted
- """
- 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 notes added",
- )
- doc.owner = user2
- doc.save()
-
- self.client.force_authenticate(user1)
-
- resp = self.client.get(
- f"/api/documents/{doc.pk}/notes/",
- format="json",
- )
- self.assertEqual(resp.content, b"Insufficient permissions to view notes")
- self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
- assign_perm("view_document", user1, doc)
-
- resp = self.client.post(
- f"/api/documents/{doc.pk}/notes/",
- data={"note": "this is a posted note"},
- )
- self.assertEqual(resp.content, b"Insufficient permissions to create notes")
- self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
- note = Note.objects.create(
- note="This is a note.",
- document=doc,
- user=user2,
- )
-
- response = self.client.delete(
- f"/api/documents/{doc.pk}/notes/?id={note.pk}",
- format="json",
- )
-
- self.assertEqual(response.content, b"Insufficient permissions to delete notes")
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_delete_note(self):
- """
- GIVEN:
- - Existing document, existing note
- WHEN:
- - API request is made to delete a note
- THEN:
- - note is deleted, document modified is updated
- """
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is a document which will have notes!",
- 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)
- note = Note.objects.create(
- note="This is a note.",
- document=doc,
- user=self.user,
- )
-
- response = self.client.delete(
- f"/api/documents/{doc.pk}/notes/?id={note.pk}",
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.assertEqual(len(Note.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_notes_no_doc(self):
- """
- GIVEN:
- - A request to get notes from a non-existent document
- WHEN:
- - API request for document notes is made
- THEN:
- - HTTP status.HTTP_404_NOT_FOUND is returned
- """
- response = self.client.get(
- "/api/documents/500/notes/",
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_tag_unique_name_and_owner(self):
- """
- GIVEN:
- - Multiple users
- - Tags owned by particular users
- WHEN:
- - API request for creating items which are unique by name and owner
- THEN:
- - Unique items are created
- - Non-unique items are not allowed
- """
- user1 = User.objects.create_user(username="test1")
- user1.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
- user1.save()
-
- user2 = User.objects.create_user(username="test2")
- user2.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
- user2.save()
-
- # User 1 creates tag 1 owned by user 1 by default
- # No issue
- self.client.force_authenticate(user1)
- response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json")
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- # User 2 creates tag 1 owned by user 2 by default
- # No issue
- self.client.force_authenticate(user2)
- response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json")
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- # User 2 creates tag 2 owned by user 1
- # No issue
- self.client.force_authenticate(user2)
- response = self.client.post(
- "/api/tags/",
- {"name": "tag 2", "owner": user1.pk},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- # User 1 creates tag 2 owned by user 1 by default
- # Not allowed, would create tag2/user1 which already exists
- self.client.force_authenticate(user1)
- response = self.client.post(
- "/api/tags/",
- {"name": "tag 2"},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- # User 1 creates tag 2 owned by user 1
- # Not allowed, would create tag2/user1 which already exists
- response = self.client.post(
- "/api/tags/",
- {"name": "tag 2", "owner": user1.pk},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_tag_unique_name_and_owner_enforced_on_update(self):
- """
- GIVEN:
- - Multiple users
- - Tags owned by particular users
- WHEN:
- - API request for to update tag in such as way as makes it non-unqiue
- THEN:
- - Unique items are created
- - Non-unique items are not allowed on update
- """
- user1 = User.objects.create_user(username="test1")
- user1.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
- user1.save()
-
- user2 = User.objects.create_user(username="test2")
- user2.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
- user2.save()
-
- # Create name tag 1 owned by user 1
- # Create name tag 1 owned by user 2
- Tag.objects.create(name="tag 1", owner=user1)
- tag2 = Tag.objects.create(name="tag 1", owner=user2)
-
- # User 2 attempts to change the owner of tag to user 1
- # Not allowed, would change to tag1/user1 which already exists
- self.client.force_authenticate(user2)
- response = self.client.patch(
- f"/api/tags/{tag2.id}/",
- {"owner": user1.pk},
- format="json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_create_share_links(self):
- """
- GIVEN:
- - Existing document
- WHEN:
- - API request is made to generate a share_link
- - API request is made to view share_links on incorrect doc pk
- - Invalid method request is made to view share_links doc
- THEN:
- - Link is created with a slug and associated with document
- - 404
- - Error
- """
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is a document which will have notes added",
- )
- # never expires
- resp = self.client.post(
- "/api/share_links/",
- data={
- "document": doc.pk,
- },
- )
- self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
-
- resp = self.client.post(
- "/api/share_links/",
- data={
- "expiration": (timezone.now() + timedelta(days=7)).isoformat(),
- "document": doc.pk,
- "file_version": "original",
- },
- )
- self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
-
- response = self.client.get(
- f"/api/documents/{doc.pk}/share_links/",
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- resp_data = response.json()
-
- self.assertEqual(len(resp_data), 2)
-
- self.assertGreater(len(resp_data[1]["slug"]), 0)
- self.assertIsNone(resp_data[1]["expiration"])
- self.assertEqual(
- (parser.isoparse(resp_data[0]["expiration"]) - timezone.now()).days,
- 6,
- )
-
- sl1 = ShareLink.objects.get(slug=resp_data[1]["slug"])
- self.assertEqual(str(sl1), f"Share Link for {doc.title}")
-
- response = self.client.post(
- f"/api/documents/{doc.pk}/share_links/",
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
-
- response = self.client.get(
- "/api/documents/99/share_links/",
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- def test_share_links_permissions_aware(self):
- """
- GIVEN:
- - Existing document owned by user2 but with granted view perms for user1
- WHEN:
- - API request is made by user1 to view share links
- THEN:
- - Links only shown if user has permissions
- """
- 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 share links added",
- )
- doc.owner = user2
- doc.save()
-
- self.client.force_authenticate(user1)
-
- resp = self.client.get(
- f"/api/documents/{doc.pk}/share_links/",
- format="json",
- )
- self.assertEqual(resp.content, b"Insufficient permissions to add share link")
- self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
- assign_perm("change_document", user1, doc)
-
- resp = self.client.get(
- f"/api/documents/{doc.pk}/share_links/",
- format="json",
- )
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
- def test_next_asn(self):
- """
- GIVEN:
- - Existing documents with ASNs, highest owned by user2
- WHEN:
- - API request is made by user1 to get next ASN
- THEN:
- - ASN +1 from user2's doc is returned for user1
- """
- 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()
-
- doc1 = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is a document 1",
- checksum="1",
- archive_serial_number=998,
- )
- doc1.owner = user1
- doc1.save()
-
- doc2 = Document.objects.create(
- title="test2",
- mime_type="application/pdf",
- content="this is a document 2 with higher ASN",
- checksum="2",
- archive_serial_number=999,
- )
- doc2.owner = user2
- doc2.save()
-
- self.client.force_authenticate(user1)
-
- resp = self.client.get(
- "/api/documents/next_asn/",
- )
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp.content, b"1000")
-
-
-class TestDocumentApiV2(DirectoriesMixin, APITestCase):
- def setUp(self):
- super().setUp()
-
- self.user = User.objects.create_superuser(username="temp_admin")
-
- self.client.force_authenticate(user=self.user)
- self.client.defaults["HTTP_ACCEPT"] = "application/json; version=2"
-
- def test_tag_validate_color(self):
- self.assertEqual(
- self.client.post(
- "/api/tags/",
- {"name": "test", "color": "#12fFaA"},
- format="json",
- ).status_code,
- status.HTTP_201_CREATED,
- )
-
- self.assertEqual(
- self.client.post(
- "/api/tags/",
- {"name": "test1", "color": "abcdef"},
- format="json",
- ).status_code,
- status.HTTP_400_BAD_REQUEST,
- )
- self.assertEqual(
- self.client.post(
- "/api/tags/",
- {"name": "test2", "color": "#abcdfg"},
- format="json",
- ).status_code,
- status.HTTP_400_BAD_REQUEST,
- )
- self.assertEqual(
- self.client.post(
- "/api/tags/",
- {"name": "test3", "color": "#asd"},
- format="json",
- ).status_code,
- status.HTTP_400_BAD_REQUEST,
- )
- self.assertEqual(
- self.client.post(
- "/api/tags/",
- {"name": "test4", "color": "#12121212"},
- format="json",
- ).status_code,
- status.HTTP_400_BAD_REQUEST,
- )
-
- def test_tag_text_color(self):
- t = Tag.objects.create(name="tag1", color="#000000")
- self.assertEqual(
- self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
- "#ffffff",
- )
-
- t.color = "#ffffff"
- t.save()
- self.assertEqual(
- self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
- "#000000",
- )
-
- t.color = "asdf"
- t.save()
- self.assertEqual(
- self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
- "#000000",
- )
-
- t.color = "123"
- t.save()
- self.assertEqual(
- self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
- "#000000",
- )
-
-
-class TestApiUiSettings(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/ui_settings/"
-
- def setUp(self):
- super().setUp()
- self.test_user = User.objects.create_superuser(username="test")
- self.test_user.first_name = "Test"
- self.test_user.last_name = "User"
- self.test_user.save()
- self.client.force_authenticate(user=self.test_user)
-
- def test_api_get_ui_settings(self):
- response = self.client.get(self.ENDPOINT, format="json")
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertDictEqual(
- response.data["user"],
- {
- "id": self.test_user.id,
- "username": self.test_user.username,
- "is_superuser": True,
- "groups": [],
- "first_name": self.test_user.first_name,
- "last_name": self.test_user.last_name,
- },
- )
- self.assertDictEqual(
- response.data["settings"],
- {
- "update_checking": {
- "backend_setting": "default",
- },
- },
- )
-
- def test_api_set_ui_settings(self):
- settings = {
- "settings": {
- "dark_mode": {
- "enabled": True,
- },
- },
- }
-
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(settings),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- ui_settings = self.test_user.ui_settings
- self.assertDictEqual(
- ui_settings.settings,
- settings["settings"],
- )
-
-
-class TestBulkEdit(DirectoriesMixin, APITestCase):
- def setUp(self):
- super().setUp()
-
- user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=user)
-
- patcher = mock.patch("documents.bulk_edit.bulk_update_documents.delay")
- self.async_task = patcher.start()
- self.addCleanup(patcher.stop)
- self.c1 = Correspondent.objects.create(name="c1")
- self.c2 = Correspondent.objects.create(name="c2")
- self.dt1 = DocumentType.objects.create(name="dt1")
- self.dt2 = DocumentType.objects.create(name="dt2")
- self.t1 = Tag.objects.create(name="t1")
- self.t2 = Tag.objects.create(name="t2")
- self.doc1 = Document.objects.create(checksum="A", title="A")
- self.doc2 = Document.objects.create(
- checksum="B",
- title="B",
- correspondent=self.c1,
- document_type=self.dt1,
- )
- self.doc3 = Document.objects.create(
- checksum="C",
- title="C",
- correspondent=self.c2,
- document_type=self.dt2,
- )
- self.doc4 = Document.objects.create(checksum="D", title="D")
- self.doc5 = Document.objects.create(checksum="E", title="E")
- self.doc2.tags.add(self.t1)
- self.doc3.tags.add(self.t2)
- self.doc4.tags.add(self.t1, self.t2)
- self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
-
- def test_set_correspondent(self):
- self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
- bulk_edit.set_correspondent(
- [self.doc1.id, self.doc2.id, self.doc3.id],
- self.c2.id,
- )
- self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3)
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
-
- def test_unset_correspondent(self):
- self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
- bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None)
- self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0)
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
-
- def test_set_document_type(self):
- self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
- bulk_edit.set_document_type(
- [self.doc1.id, self.doc2.id, self.doc3.id],
- self.dt2.id,
- )
- self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3)
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
-
- def test_unset_document_type(self):
- self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
- bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None)
- self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0)
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
-
- def test_set_document_storage_path(self):
- """
- GIVEN:
- - 5 documents without defined storage path
- WHEN:
- - Bulk edit called to add storage path to 1 document
- THEN:
- - Single document storage path update
- """
- self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
-
- bulk_edit.set_storage_path(
- [self.doc1.id],
- self.sp1.id,
- )
-
- self.assertEqual(Document.objects.filter(storage_path=None).count(), 4)
-
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
-
- self.assertCountEqual(kwargs["document_ids"], [self.doc1.id])
-
- def test_unset_document_storage_path(self):
- """
- GIVEN:
- - 4 documents without defined storage path
- - 1 document with a defined storage
- WHEN:
- - Bulk edit called to remove storage path from 1 document
- THEN:
- - Single document storage path removed
- """
- self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
-
- bulk_edit.set_storage_path(
- [self.doc1.id],
- self.sp1.id,
- )
-
- self.assertEqual(Document.objects.filter(storage_path=None).count(), 4)
-
- bulk_edit.set_storage_path(
- [self.doc1.id],
- None,
- )
-
- self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
-
- self.async_task.assert_called()
- args, kwargs = self.async_task.call_args
-
- self.assertCountEqual(kwargs["document_ids"], [self.doc1.id])
-
- def test_add_tag(self):
- self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
- bulk_edit.add_tag(
- [self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id],
- self.t1.id,
- )
- self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4)
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc3.id])
-
- def test_remove_tag(self):
- self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
- bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id)
- self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1)
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- self.assertCountEqual(kwargs["document_ids"], [self.doc4.id])
-
- def test_modify_tags(self):
- tag_unrelated = Tag.objects.create(name="unrelated")
- self.doc2.tags.add(tag_unrelated)
- self.doc3.tags.add(tag_unrelated)
- bulk_edit.modify_tags(
- [self.doc2.id, self.doc3.id],
- add_tags=[self.t2.id],
- remove_tags=[self.t1.id],
- )
-
- self.assertCountEqual(list(self.doc2.tags.all()), [self.t2, tag_unrelated])
- self.assertCountEqual(list(self.doc3.tags.all()), [self.t2, tag_unrelated])
-
- self.async_task.assert_called_once()
- args, kwargs = self.async_task.call_args
- # TODO: doc3 should not be affected, but the query for that is rather complicated
- self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
-
- def test_delete(self):
- self.assertEqual(Document.objects.count(), 5)
- bulk_edit.delete([self.doc1.id, self.doc2.id])
- self.assertEqual(Document.objects.count(), 3)
- self.assertCountEqual(
- [doc.id for doc in Document.objects.all()],
- [self.doc3.id, self.doc4.id, self.doc5.id],
- )
-
- @mock.patch("documents.serialisers.bulk_edit.set_correspondent")
- def test_api_set_correspondent(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_correspondent",
- "parameters": {"correspondent": self.c1.id},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertEqual(kwargs["correspondent"], self.c1.id)
-
- @mock.patch("documents.serialisers.bulk_edit.set_correspondent")
- def test_api_unset_correspondent(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_correspondent",
- "parameters": {"correspondent": None},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertIsNone(kwargs["correspondent"])
-
- @mock.patch("documents.serialisers.bulk_edit.set_document_type")
- def test_api_set_type(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_document_type",
- "parameters": {"document_type": self.dt1.id},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertEqual(kwargs["document_type"], self.dt1.id)
-
- @mock.patch("documents.serialisers.bulk_edit.set_document_type")
- def test_api_unset_type(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_document_type",
- "parameters": {"document_type": None},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertIsNone(kwargs["document_type"])
-
- @mock.patch("documents.serialisers.bulk_edit.add_tag")
- def test_api_add_tag(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "add_tag",
- "parameters": {"tag": self.t1.id},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertEqual(kwargs["tag"], self.t1.id)
-
- @mock.patch("documents.serialisers.bulk_edit.remove_tag")
- def test_api_remove_tag(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "remove_tag",
- "parameters": {"tag": self.t1.id},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertEqual(kwargs["tag"], self.t1.id)
-
- @mock.patch("documents.serialisers.bulk_edit.modify_tags")
- def test_api_modify_tags(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id, self.doc3.id],
- "method": "modify_tags",
- "parameters": {
- "add_tags": [self.t1.id],
- "remove_tags": [self.t2.id],
- },
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertListEqual(args[0], [self.doc1.id, self.doc3.id])
- self.assertEqual(kwargs["add_tags"], [self.t1.id])
- self.assertEqual(kwargs["remove_tags"], [self.t2.id])
-
- @mock.patch("documents.serialisers.bulk_edit.modify_tags")
- def test_api_modify_tags_not_provided(self, m):
- """
- GIVEN:
- - API data to modify tags is missing modify_tags field
- WHEN:
- - API to edit tags is called
- THEN:
- - API returns HTTP 400
- - modify_tags is not called
- """
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id, self.doc3.id],
- "method": "modify_tags",
- "parameters": {
- "add_tags": [self.t1.id],
- },
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- m.assert_not_called()
-
- @mock.patch("documents.serialisers.bulk_edit.delete")
- def test_api_delete(self, m):
- m.return_value = "OK"
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {"documents": [self.doc1.id], "method": "delete", "parameters": {}},
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertEqual(args[0], [self.doc1.id])
- self.assertEqual(len(kwargs), 0)
-
- @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
- def test_api_set_storage_path(self, m):
- """
- GIVEN:
- - API data to set the storage path of a document
- WHEN:
- - API is called
- THEN:
- - set_storage_path is called with correct document IDs and storage_path ID
- """
- m.return_value = "OK"
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_storage_path",
- "parameters": {"storage_path": self.sp1.id},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
-
- self.assertListEqual(args[0], [self.doc1.id])
- self.assertEqual(kwargs["storage_path"], self.sp1.id)
-
- @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
- def test_api_unset_storage_path(self, m):
- """
- GIVEN:
- - API data to clear/unset the storage path of a document
- WHEN:
- - API is called
- THEN:
- - set_storage_path is called with correct document IDs and None storage_path
- """
- m.return_value = "OK"
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_storage_path",
- "parameters": {"storage_path": None},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
- args, kwargs = m.call_args
-
- self.assertListEqual(args[0], [self.doc1.id])
- self.assertEqual(kwargs["storage_path"], None)
-
- def test_api_invalid_storage_path(self):
- """
- GIVEN:
- - API data to set the storage path of a document
- - Given storage_path ID isn't valid
- WHEN:
- - API is called
- THEN:
- - set_storage_path is called with correct document IDs and storage_path ID
- """
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_storage_path",
- "parameters": {"storage_path": self.sp1.id + 10},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.async_task.assert_not_called()
-
- def test_api_set_storage_path_not_provided(self):
- """
- GIVEN:
- - API data to set the storage path of a document
- - API data is missing storage path ID
- WHEN:
- - API is called
- THEN:
- - set_storage_path is called with correct document IDs and storage_path ID
- """
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id],
- "method": "set_storage_path",
- "parameters": {},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.async_task.assert_not_called()
-
- def test_api_invalid_doc(self):
- self.assertEqual(Document.objects.count(), 5)
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps({"documents": [-235], "method": "delete", "parameters": {}}),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(Document.objects.count(), 5)
-
- def test_api_invalid_method(self):
- self.assertEqual(Document.objects.count(), 5)
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "exterminate",
- "parameters": {},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(Document.objects.count(), 5)
-
- def test_api_invalid_correspondent(self):
- self.assertEqual(self.doc2.correspondent, self.c1)
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "set_correspondent",
- "parameters": {"correspondent": 345657},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- doc2 = Document.objects.get(id=self.doc2.id)
- self.assertEqual(doc2.correspondent, self.c1)
-
- def test_api_no_correspondent(self):
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "set_correspondent",
- "parameters": {},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_api_invalid_document_type(self):
- self.assertEqual(self.doc2.document_type, self.dt1)
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "set_document_type",
- "parameters": {"document_type": 345657},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- doc2 = Document.objects.get(id=self.doc2.id)
- self.assertEqual(doc2.document_type, self.dt1)
-
- def test_api_no_document_type(self):
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "set_document_type",
- "parameters": {},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_api_add_invalid_tag(self):
- self.assertEqual(list(self.doc2.tags.all()), [self.t1])
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "add_tag",
- "parameters": {"tag": 345657},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- self.assertEqual(list(self.doc2.tags.all()), [self.t1])
-
- def test_api_add_tag_no_tag(self):
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {"documents": [self.doc2.id], "method": "add_tag", "parameters": {}},
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_api_delete_invalid_tag(self):
- self.assertEqual(list(self.doc2.tags.all()), [self.t1])
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "remove_tag",
- "parameters": {"tag": 345657},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- self.assertEqual(list(self.doc2.tags.all()), [self.t1])
-
- def test_api_delete_tag_no_tag(self):
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {"documents": [self.doc2.id], "method": "remove_tag", "parameters": {}},
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_api_modify_invalid_tags(self):
- self.assertEqual(list(self.doc2.tags.all()), [self.t1])
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "modify_tags",
- "parameters": {
- "add_tags": [self.t2.id, 1657],
- "remove_tags": [1123123],
- },
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_api_modify_tags_no_tags(self):
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "modify_tags",
- "parameters": {"remove_tags": [1123123]},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id],
- "method": "modify_tags",
- "parameters": {"add_tags": [self.t2.id, 1657]},
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_api_selection_data_empty(self):
- response = self.client.post(
- "/api/documents/selection_data/",
- json.dumps({"documents": []}),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- for field, Entity in [
- ("selected_correspondents", Correspondent),
- ("selected_tags", Tag),
- ("selected_document_types", DocumentType),
- ]:
- self.assertEqual(len(response.data[field]), Entity.objects.count())
- for correspondent in response.data[field]:
- self.assertEqual(correspondent["document_count"], 0)
- self.assertCountEqual(
- map(lambda c: c["id"], response.data[field]),
- map(lambda c: c["id"], Entity.objects.values("id")),
- )
-
- def test_api_selection_data(self):
- response = self.client.post(
- "/api/documents/selection_data/",
- json.dumps(
- {"documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id]},
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- self.assertCountEqual(
- response.data["selected_correspondents"],
- [
- {"id": self.c1.id, "document_count": 1},
- {"id": self.c2.id, "document_count": 0},
- ],
- )
- self.assertCountEqual(
- response.data["selected_tags"],
- [
- {"id": self.t1.id, "document_count": 2},
- {"id": self.t2.id, "document_count": 1},
- ],
- )
- self.assertCountEqual(
- response.data["selected_document_types"],
- [
- {"id": self.c1.id, "document_count": 1},
- {"id": self.c2.id, "document_count": 0},
- ],
- )
-
- @mock.patch("documents.serialisers.bulk_edit.set_permissions")
- def test_set_permissions(self, m):
- m.return_value = "OK"
- user1 = User.objects.create(username="user1")
- user2 = User.objects.create(username="user2")
- permissions = {
- "view": {
- "users": [user1.id, user2.id],
- "groups": None,
- },
- "change": {
- "users": [user1.id],
- "groups": None,
- },
- }
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id, self.doc3.id],
- "method": "set_permissions",
- "parameters": {"set_permissions": permissions},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- m.assert_called_once()
- args, kwargs = m.call_args
- self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
- self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
-
- @mock.patch("documents.serialisers.bulk_edit.set_permissions")
- def test_insufficient_permissions_ownership(self, m):
- """
- GIVEN:
- - Documents owned by user other than logged in user
- WHEN:
- - set_permissions bulk edit API endpoint is called
- THEN:
- - User is not able to change permissions
- """
- m.return_value = "OK"
- self.doc1.owner = User.objects.get(username="temp_admin")
- self.doc1.save()
- user1 = User.objects.create(username="user1")
- self.client.force_authenticate(user=user1)
-
- permissions = {
- "owner": user1.id,
- }
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
- "method": "set_permissions",
- "parameters": {"set_permissions": permissions},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- m.assert_not_called()
- self.assertEqual(response.content, b"Insufficient permissions")
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc2.id, self.doc3.id],
- "method": "set_permissions",
- "parameters": {"set_permissions": permissions},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- m.assert_called_once()
-
- @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
- def test_insufficient_permissions_edit(self, m):
- """
- GIVEN:
- - Documents for which current user only has view permissions
- WHEN:
- - API is called
- THEN:
- - set_storage_path is only called if user can edit all docs
- """
- m.return_value = "OK"
- self.doc1.owner = User.objects.get(username="temp_admin")
- self.doc1.save()
- user1 = User.objects.create(username="user1")
- assign_perm("view_document", user1, self.doc1)
- self.client.force_authenticate(user=user1)
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
- "method": "set_storage_path",
- "parameters": {"storage_path": self.sp1.id},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- m.assert_not_called()
- self.assertEqual(response.content, b"Insufficient permissions")
-
- assign_perm("change_document", user1, self.doc1)
-
- response = self.client.post(
- "/api/documents/bulk_edit/",
- json.dumps(
- {
- "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
- "method": "set_storage_path",
- "parameters": {"storage_path": self.sp1.id},
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- m.assert_called_once()
-
-
-class TestBulkDownload(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/documents/bulk_download/"
-
- def setUp(self):
- super().setUp()
-
- user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=user)
-
- self.doc1 = Document.objects.create(title="unrelated", checksum="A")
- self.doc2 = Document.objects.create(
- title="document A",
- filename="docA.pdf",
- mime_type="application/pdf",
- checksum="B",
- created=timezone.make_aware(datetime.datetime(2021, 1, 1)),
- )
- self.doc2b = Document.objects.create(
- title="document A",
- filename="docA2.pdf",
- mime_type="application/pdf",
- checksum="D",
- created=timezone.make_aware(datetime.datetime(2021, 1, 1)),
- )
- self.doc3 = Document.objects.create(
- title="document B",
- filename="docB.jpg",
- mime_type="image/jpeg",
- checksum="C",
- created=timezone.make_aware(datetime.datetime(2020, 3, 21)),
- archive_filename="docB.pdf",
- archive_checksum="D",
- )
-
- shutil.copy(
- os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
- self.doc2.source_path,
- )
- shutil.copy(
- os.path.join(os.path.dirname(__file__), "samples", "simple.png"),
- self.doc2b.source_path,
- )
- shutil.copy(
- os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"),
- self.doc3.source_path,
- )
- shutil.copy(
- os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"),
- self.doc3.archive_path,
- )
-
- def test_download_originals(self):
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {"documents": [self.doc2.id, self.doc3.id], "content": "originals"},
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 2)
- self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
- self.assertIn("2020-03-21 document B.jpg", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
-
- with self.doc3.source_file as f:
- self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg"))
-
- def test_download_default(self):
- response = self.client.post(
- self.ENDPOINT,
- json.dumps({"documents": [self.doc2.id, self.doc3.id]}),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 2)
- self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
- self.assertIn("2020-03-21 document B.pdf", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
-
- with self.doc3.archive_file as f:
- self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf"))
-
- def test_download_both(self):
- response = self.client.post(
- self.ENDPOINT,
- json.dumps({"documents": [self.doc2.id, self.doc3.id], "content": "both"}),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 3)
- self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist())
- self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist())
- self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("originals/2021-01-01 document A.pdf"),
- )
-
- with self.doc3.archive_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("archive/2020-03-21 document B.pdf"),
- )
-
- with self.doc3.source_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("originals/2020-03-21 document B.jpg"),
- )
-
- def test_filename_clashes(self):
- response = self.client.post(
- self.ENDPOINT,
- json.dumps({"documents": [self.doc2.id, self.doc2b.id]}),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 2)
-
- self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
- self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
-
- with self.doc2b.source_file as f:
- self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf"))
-
- def test_compression(self):
- self.client.post(
- self.ENDPOINT,
- json.dumps(
- {"documents": [self.doc2.id, self.doc2b.id], "compression": "lzma"},
- ),
- content_type="application/json",
- )
-
- @override_settings(FILENAME_FORMAT="{correspondent}/{title}")
- def test_formatted_download_originals(self):
- """
- GIVEN:
- - Defined file naming format
- WHEN:
- - Bulk download request for original documents
- - Bulk download request requests to follow format
- THEN:
- - Files defined in resulting zipfile are formatted
- """
-
- c = Correspondent.objects.create(name="test")
- c2 = Correspondent.objects.create(name="a space name")
-
- self.doc2.correspondent = c
- self.doc2.title = "This is Doc 2"
- self.doc2.save()
-
- self.doc3.correspondent = c2
- self.doc3.title = "Title 2 - Doc 3"
- self.doc3.save()
-
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "documents": [self.doc2.id, self.doc3.id],
- "content": "originals",
- "follow_formatting": True,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 2)
- self.assertIn("a space name/Title 2 - Doc 3.jpg", zipf.namelist())
- self.assertIn("test/This is Doc 2.pdf", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(f.read(), zipf.read("test/This is Doc 2.pdf"))
-
- with self.doc3.source_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("a space name/Title 2 - Doc 3.jpg"),
- )
-
- @override_settings(FILENAME_FORMAT="somewhere/{title}")
- def test_formatted_download_archive(self):
- """
- GIVEN:
- - Defined file naming format
- WHEN:
- - Bulk download request for archive documents
- - Bulk download request requests to follow format
- THEN:
- - Files defined in resulting zipfile are formatted
- """
-
- self.doc2.title = "This is Doc 2"
- self.doc2.save()
-
- self.doc3.title = "Title 2 - Doc 3"
- self.doc3.save()
- print(self.doc3.archive_path)
- print(self.doc3.archive_filename)
-
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "documents": [self.doc2.id, self.doc3.id],
- "follow_formatting": True,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 2)
- self.assertIn("somewhere/This is Doc 2.pdf", zipf.namelist())
- self.assertIn("somewhere/Title 2 - Doc 3.pdf", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(f.read(), zipf.read("somewhere/This is Doc 2.pdf"))
-
- with self.doc3.archive_file as f:
- self.assertEqual(f.read(), zipf.read("somewhere/Title 2 - Doc 3.pdf"))
-
- @override_settings(FILENAME_FORMAT="{document_type}/{title}")
- def test_formatted_download_both(self):
- """
- GIVEN:
- - Defined file naming format
- WHEN:
- - Bulk download request for original documents and archive documents
- - Bulk download request requests to follow format
- THEN:
- - Files defined in resulting zipfile are formatted
- """
-
- dc1 = DocumentType.objects.create(name="bill")
- dc2 = DocumentType.objects.create(name="statement")
-
- self.doc2.document_type = dc1
- self.doc2.title = "This is Doc 2"
- self.doc2.save()
-
- self.doc3.document_type = dc2
- self.doc3.title = "Title 2 - Doc 3"
- self.doc3.save()
-
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "documents": [self.doc2.id, self.doc3.id],
- "content": "both",
- "follow_formatting": True,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response["Content-Type"], "application/zip")
-
- with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
- self.assertEqual(len(zipf.filelist), 3)
- self.assertIn("originals/bill/This is Doc 2.pdf", zipf.namelist())
- self.assertIn("archive/statement/Title 2 - Doc 3.pdf", zipf.namelist())
- self.assertIn("originals/statement/Title 2 - Doc 3.jpg", zipf.namelist())
-
- with self.doc2.source_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("originals/bill/This is Doc 2.pdf"),
- )
-
- with self.doc3.archive_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("archive/statement/Title 2 - Doc 3.pdf"),
- )
-
- with self.doc3.source_file as f:
- self.assertEqual(
- f.read(),
- zipf.read("originals/statement/Title 2 - Doc 3.jpg"),
- )
-
-
-class TestApiAuth(DirectoriesMixin, APITestCase):
- def test_auth_required(self):
- d = Document.objects.create(title="Test")
-
- self.assertEqual(
- self.client.get("/api/documents/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
-
- self.assertEqual(
- self.client.get(f"/api/documents/{d.id}/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get(f"/api/documents/{d.id}/download/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get(f"/api/documents/{d.id}/preview/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get(f"/api/documents/{d.id}/thumb/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
-
- self.assertEqual(
- self.client.get("/api/tags/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get("/api/correspondents/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get("/api/document_types/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
-
- self.assertEqual(
- self.client.get("/api/logs/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get("/api/saved_views/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
-
- self.assertEqual(
- self.client.get("/api/search/autocomplete/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get("/api/documents/bulk_edit/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get("/api/documents/bulk_download/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
- self.assertEqual(
- self.client.get("/api/documents/selection_data/").status_code,
- status.HTTP_401_UNAUTHORIZED,
- )
-
- def test_api_version_no_auth(self):
- response = self.client.get("/api/")
- self.assertNotIn("X-Api-Version", response)
- self.assertNotIn("X-Version", response)
-
- def test_api_version_with_auth(self):
- user = User.objects.create_superuser(username="test")
- self.client.force_authenticate(user)
- response = self.client.get("/api/")
- self.assertIn("X-Api-Version", response)
- self.assertIn("X-Version", response)
-
- def test_api_insufficient_permissions(self):
- user = User.objects.create_user(username="test")
- self.client.force_authenticate(user)
-
- Document.objects.create(title="Test")
-
- self.assertEqual(
- self.client.get("/api/documents/").status_code,
- status.HTTP_403_FORBIDDEN,
- )
-
- self.assertEqual(
- self.client.get("/api/tags/").status_code,
- status.HTTP_403_FORBIDDEN,
- )
- self.assertEqual(
- self.client.get("/api/correspondents/").status_code,
- status.HTTP_403_FORBIDDEN,
- )
- self.assertEqual(
- self.client.get("/api/document_types/").status_code,
- status.HTTP_403_FORBIDDEN,
- )
-
- self.assertEqual(
- self.client.get("/api/logs/").status_code,
- status.HTTP_403_FORBIDDEN,
- )
- self.assertEqual(
- self.client.get("/api/saved_views/").status_code,
- status.HTTP_403_FORBIDDEN,
- )
-
- def test_api_sufficient_permissions(self):
- user = User.objects.create_user(username="test")
- user.user_permissions.add(*Permission.objects.all())
- self.client.force_authenticate(user)
-
- Document.objects.create(title="Test")
-
- self.assertEqual(
- self.client.get("/api/documents/").status_code,
- status.HTTP_200_OK,
- )
-
- self.assertEqual(self.client.get("/api/tags/").status_code, status.HTTP_200_OK)
- self.assertEqual(
- self.client.get("/api/correspondents/").status_code,
- status.HTTP_200_OK,
- )
- self.assertEqual(
- self.client.get("/api/document_types/").status_code,
- status.HTTP_200_OK,
- )
-
- self.assertEqual(self.client.get("/api/logs/").status_code, status.HTTP_200_OK)
- self.assertEqual(
- self.client.get("/api/saved_views/").status_code,
- status.HTTP_200_OK,
- )
-
- def test_api_get_object_permissions(self):
- user1 = User.objects.create_user(username="test1")
- user2 = User.objects.create_user(username="test2")
- user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
- self.client.force_authenticate(user1)
-
- self.assertEqual(
- self.client.get("/api/documents/").status_code,
- status.HTTP_200_OK,
- )
-
- d = Document.objects.create(title="Test", content="the content 1", checksum="1")
-
- # no owner
- self.assertEqual(
- self.client.get(f"/api/documents/{d.id}/").status_code,
- status.HTTP_200_OK,
- )
-
- d2 = Document.objects.create(
- title="Test 2",
- content="the content 2",
- checksum="2",
- owner=user2,
- )
-
- self.assertEqual(
- self.client.get(f"/api/documents/{d2.id}/").status_code,
- status.HTTP_404_NOT_FOUND,
- )
-
- def test_api_default_owner(self):
- """
- GIVEN:
- - API request to create an object (Tag)
- WHEN:
- - owner is not set at all
- THEN:
- - Object created with current user as owner
- """
- user1 = User.objects.create_superuser(username="user1")
-
- self.client.force_authenticate(user1)
-
- response = self.client.post(
- "/api/tags/",
- json.dumps(
- {
- "name": "test1",
- "matching_algorithm": MatchingModel.MATCH_AUTO,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- tag1 = Tag.objects.filter(name="test1").first()
- self.assertEqual(tag1.owner, user1)
-
- def test_api_set_no_owner(self):
- """
- GIVEN:
- - API request to create an object (Tag)
- WHEN:
- - owner is passed as None
- THEN:
- - Object created with no owner
- """
- user1 = User.objects.create_superuser(username="user1")
-
- self.client.force_authenticate(user1)
-
- response = self.client.post(
- "/api/tags/",
- json.dumps(
- {
- "name": "test1",
- "matching_algorithm": MatchingModel.MATCH_AUTO,
- "owner": None,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- tag1 = Tag.objects.filter(name="test1").first()
- self.assertEqual(tag1.owner, None)
-
- def test_api_set_owner_w_permissions(self):
- """
- GIVEN:
- - API request to create an object (Tag) that supplies set_permissions object
- WHEN:
- - owner is passed as user id
- - view > users is set & view > groups is set
- THEN:
- - Object permissions are set appropriately
- """
- user1 = User.objects.create_superuser(username="user1")
- user2 = User.objects.create(username="user2")
- group1 = Group.objects.create(name="group1")
-
- self.client.force_authenticate(user1)
-
- response = self.client.post(
- "/api/tags/",
- json.dumps(
- {
- "name": "test1",
- "matching_algorithm": MatchingModel.MATCH_AUTO,
- "owner": user1.id,
- "set_permissions": {
- "view": {
- "users": [user2.id],
- "groups": [group1.id],
- },
- "change": {
- "users": None,
- "groups": None,
- },
- },
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- tag1 = Tag.objects.filter(name="test1").first()
-
- from guardian.core import ObjectPermissionChecker
-
- checker = ObjectPermissionChecker(user2)
- self.assertEqual(checker.has_perm("view_tag", tag1), True)
- self.assertIn("view_tag", get_perms(group1, tag1))
-
- def test_api_set_other_owner_w_permissions(self):
- """
- GIVEN:
- - API request to create an object (Tag)
- WHEN:
- - a different owner than is logged in is set
- - view > groups is set
- THEN:
- - Object permissions are set appropriately
- """
- user1 = User.objects.create_superuser(username="user1")
- user2 = User.objects.create(username="user2")
- group1 = Group.objects.create(name="group1")
-
- self.client.force_authenticate(user1)
-
- response = self.client.post(
- "/api/tags/",
- json.dumps(
- {
- "name": "test1",
- "matching_algorithm": MatchingModel.MATCH_AUTO,
- "owner": user2.id,
- "set_permissions": {
- "view": {
- "users": None,
- "groups": [group1.id],
- },
- "change": {
- "users": None,
- "groups": None,
- },
- },
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- tag1 = Tag.objects.filter(name="test1").first()
-
- self.assertEqual(tag1.owner, user2)
- self.assertIn("view_tag", get_perms(group1, tag1))
-
- def test_api_set_doc_permissions(self):
- """
- GIVEN:
- - API request to update doc permissions and owner
- WHEN:
- - owner is set
- - view > users is set & view > groups is set
- THEN:
- - Object permissions are set appropriately
- """
- doc = Document.objects.create(
- title="test",
- mime_type="application/pdf",
- content="this is a document",
- )
- user1 = User.objects.create_superuser(username="user1")
- user2 = User.objects.create(username="user2")
- group1 = Group.objects.create(name="group1")
-
- self.client.force_authenticate(user1)
-
- response = self.client.patch(
- f"/api/documents/{doc.id}/",
- json.dumps(
- {
- "owner": user1.id,
- "set_permissions": {
- "view": {
- "users": [user2.id],
- "groups": [group1.id],
- },
- "change": {
- "users": None,
- "groups": None,
- },
- },
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- doc = Document.objects.get(pk=doc.id)
-
- self.assertEqual(doc.owner, user1)
- from guardian.core import ObjectPermissionChecker
-
- checker = ObjectPermissionChecker(user2)
- self.assertTrue(checker.has_perm("view_document", doc))
- self.assertIn("view_document", get_perms(group1, doc))
-
- def test_dynamic_permissions_fields(self):
- user1 = User.objects.create_user(username="user1")
- user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
- user2 = User.objects.create_user(username="user2")
-
- Document.objects.create(title="Test", content="content 1", checksum="1")
- doc2 = Document.objects.create(
- title="Test2",
- content="content 2",
- checksum="2",
- owner=user2,
- )
- doc3 = Document.objects.create(
- title="Test3",
- content="content 3",
- checksum="3",
- owner=user2,
- )
-
- assign_perm("view_document", user1, doc2)
- assign_perm("view_document", user1, doc3)
- assign_perm("change_document", user1, doc3)
-
- self.client.force_authenticate(user1)
-
- response = self.client.get(
- "/api/documents/",
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- resp_data = response.json()
-
- self.assertNotIn("permissions", resp_data["results"][0])
- self.assertIn("user_can_change", resp_data["results"][0])
- self.assertEqual(resp_data["results"][0]["user_can_change"], True) # doc1
- self.assertEqual(resp_data["results"][1]["user_can_change"], False) # doc2
- self.assertEqual(resp_data["results"][2]["user_can_change"], True) # doc3
-
- response = self.client.get(
- "/api/documents/?full_perms=true",
- format="json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- resp_data = response.json()
-
- self.assertIn("permissions", resp_data["results"][0])
- self.assertNotIn("user_can_change", resp_data["results"][0])
-
-
-class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/remote_version/"
-
- def setUp(self):
- super().setUp()
-
- @mock.patch("urllib.request.urlopen")
- def test_remote_version_enabled_no_update_prefix(self, urlopen_mock):
- cm = MagicMock()
- cm.getcode.return_value = status.HTTP_200_OK
- cm.read.return_value = json.dumps({"tag_name": "ngx-1.6.0"}).encode()
- cm.__enter__.return_value = cm
- urlopen_mock.return_value = cm
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertDictEqual(
- response.data,
- {
- "version": "1.6.0",
- "update_available": False,
- },
- )
-
- @mock.patch("urllib.request.urlopen")
- def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock):
- cm = MagicMock()
- cm.getcode.return_value = status.HTTP_200_OK
- cm.read.return_value = json.dumps(
- {"tag_name": version.__full_version_str__},
- ).encode()
- cm.__enter__.return_value = cm
- urlopen_mock.return_value = cm
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertDictEqual(
- response.data,
- {
- "version": version.__full_version_str__,
- "update_available": False,
- },
- )
-
- @mock.patch("urllib.request.urlopen")
- def test_remote_version_enabled_update(self, urlopen_mock):
- new_version = (
- version.__version__[0],
- version.__version__[1],
- version.__version__[2] + 1,
- )
- new_version_str = ".".join(map(str, new_version))
-
- cm = MagicMock()
- cm.getcode.return_value = status.HTTP_200_OK
- cm.read.return_value = json.dumps(
- {"tag_name": new_version_str},
- ).encode()
- cm.__enter__.return_value = cm
- urlopen_mock.return_value = cm
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertDictEqual(
- response.data,
- {
- "version": new_version_str,
- "update_available": True,
- },
- )
-
- @mock.patch("urllib.request.urlopen")
- def test_remote_version_bad_json(self, urlopen_mock):
- cm = MagicMock()
- cm.getcode.return_value = status.HTTP_200_OK
- cm.read.return_value = b'{ "blah":'
- cm.__enter__.return_value = cm
- urlopen_mock.return_value = cm
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertDictEqual(
- response.data,
- {
- "version": "0.0.0",
- "update_available": False,
- },
- )
-
- @mock.patch("urllib.request.urlopen")
- def test_remote_version_exception(self, urlopen_mock):
- cm = MagicMock()
- cm.getcode.return_value = status.HTTP_200_OK
- cm.read.side_effect = urllib.error.URLError("an error")
- cm.__enter__.return_value = cm
- urlopen_mock.return_value = cm
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertDictEqual(
- response.data,
- {
- "version": "0.0.0",
- "update_available": False,
- },
- )
-
-
-class TestApiObjects(DirectoriesMixin, APITestCase):
- def setUp(self) -> None:
- super().setUp()
-
- user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=user)
-
- self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True)
- self.tag2 = Tag.objects.create(name="t2")
- self.tag3 = Tag.objects.create(name="t3")
- self.c1 = Correspondent.objects.create(name="c1")
- self.c2 = Correspondent.objects.create(name="c2")
- self.c3 = Correspondent.objects.create(name="c3")
- self.dt1 = DocumentType.objects.create(name="dt1")
- self.dt2 = DocumentType.objects.create(name="dt2")
- self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{title}")
- self.sp2 = StoragePath.objects.create(name="sp2", path="Something2/{title}")
-
- def test_object_filters(self):
- response = self.client.get(
- f"/api/tags/?id={self.tag2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
-
- response = self.client.get(
- f"/api/tags/?id__in={self.tag1.id},{self.tag3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
-
- response = self.client.get(
- f"/api/correspondents/?id={self.c2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
-
- response = self.client.get(
- f"/api/correspondents/?id__in={self.c1.id},{self.c3.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
-
- response = self.client.get(
- f"/api/document_types/?id={self.dt1.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
-
- response = self.client.get(
- f"/api/document_types/?id__in={self.dt1.id},{self.dt2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
-
- response = self.client.get(
- f"/api/storage_paths/?id={self.sp1.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 1)
-
- response = self.client.get(
- f"/api/storage_paths/?id__in={self.sp1.id},{self.sp2.id}",
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- results = response.data["results"]
- self.assertEqual(len(results), 2)
-
-
-class TestApiStoragePaths(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/storage_paths/"
-
- def setUp(self) -> None:
- super().setUp()
-
- user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=user)
-
- self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
-
- def test_api_get_storage_path(self):
- """
- GIVEN:
- - API request to get all storage paths
- WHEN:
- - API is called
- THEN:
- - Existing storage paths are returned
- """
- response = self.client.get(self.ENDPOINT, format="json")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 1)
-
- resp_storage_path = response.data["results"][0]
- self.assertEqual(resp_storage_path["id"], self.sp1.id)
- self.assertEqual(resp_storage_path["path"], self.sp1.path)
-
- def test_api_create_storage_path(self):
- """
- GIVEN:
- - API request to create a storage paths
- WHEN:
- - API is called
- THEN:
- - Correct HTTP response
- - New storage path is created
- """
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "A storage path",
- "path": "Somewhere/{asn}",
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(StoragePath.objects.count(), 2)
-
- def test_api_create_invalid_storage_path(self):
- """
- GIVEN:
- - API request to create a storage paths
- - Storage path format is incorrect
- WHEN:
- - API is called
- THEN:
- - Correct HTTP 400 response
- - No storage path is created
- """
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Another storage path",
- "path": "Somewhere/{correspdent}",
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(StoragePath.objects.count(), 1)
-
- def test_api_storage_path_placeholders(self):
- """
- GIVEN:
- - API request to create a storage path with placeholders
- - Storage path is valid
- WHEN:
- - API is called
- THEN:
- - Correct HTTP response
- - New storage path is created
- """
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Storage path with placeholders",
- "path": "{title}/{correspondent}/{document_type}/{created}/{created_year}"
- "/{created_year_short}/{created_month}/{created_month_name}"
- "/{created_month_name_short}/{created_day}/{added}/{added_year}"
- "/{added_year_short}/{added_month}/{added_month_name}"
- "/{added_month_name_short}/{added_day}/{asn}/{tags}"
- "/{tag_list}/{owner_username}/{original_name}/{doc_pk}/",
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(StoragePath.objects.count(), 2)
-
- @mock.patch("documents.bulk_edit.bulk_update_documents.delay")
- def test_api_update_storage_path(self, bulk_update_mock):
- """
- GIVEN:
- - API request to get all storage paths
- WHEN:
- - API is called
- THEN:
- - Existing storage paths are returned
- """
- document = Document.objects.create(
- mime_type="application/pdf",
- storage_path=self.sp1,
- )
- response = self.client.patch(
- f"{self.ENDPOINT}{self.sp1.pk}/",
- data={
- "path": "somewhere/{created} - {title}",
- },
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- bulk_update_mock.assert_called_once()
-
- args, _ = bulk_update_mock.call_args
-
- self.assertCountEqual([document.pk], args[0])
-
-
-class TestTasks(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/tasks/"
- ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/"
-
- def setUp(self):
- super().setUp()
-
- self.user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=self.user)
-
- def test_get_tasks(self):
- """
- GIVEN:
- - Attempted celery tasks
- WHEN:
- - API call is made to get tasks
- THEN:
- - Attempting and pending tasks are serialized and provided
- """
-
- task1 = PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_one.pdf",
- )
-
- task2 = PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_two.pdf",
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 2)
- returned_task1 = response.data[1]
- returned_task2 = response.data[0]
-
- self.assertEqual(returned_task1["task_id"], task1.task_id)
- self.assertEqual(returned_task1["status"], celery.states.PENDING)
- self.assertEqual(returned_task1["task_file_name"], task1.task_file_name)
-
- self.assertEqual(returned_task2["task_id"], task2.task_id)
- self.assertEqual(returned_task2["status"], celery.states.PENDING)
- self.assertEqual(returned_task2["task_file_name"], task2.task_file_name)
-
- def test_get_single_task_status(self):
- """
- GIVEN
- - Query parameter for a valid task ID
- WHEN:
- - API call is made to get task status
- THEN:
- - Single task data is returned
- """
-
- id1 = str(uuid.uuid4())
- task1 = PaperlessTask.objects.create(
- task_id=id1,
- task_file_name="task_one.pdf",
- )
-
- _ = PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_two.pdf",
- )
-
- response = self.client.get(self.ENDPOINT + f"?task_id={id1}")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
- returned_task1 = response.data[0]
-
- self.assertEqual(returned_task1["task_id"], task1.task_id)
-
- def test_get_single_task_status_not_valid(self):
- """
- GIVEN
- - Query parameter for a non-existent task ID
- WHEN:
- - API call is made to get task status
- THEN:
- - No task data is returned
- """
- PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_one.pdf",
- )
-
- _ = PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_two.pdf",
- )
-
- response = self.client.get(self.ENDPOINT + "?task_id=bad-task-id")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 0)
-
- def test_acknowledge_tasks(self):
- """
- GIVEN:
- - Attempted celery tasks
- WHEN:
- - API call is made to get mark task as acknowledged
- THEN:
- - Task is marked as acknowledged
- """
- task = PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_one.pdf",
- )
-
- response = self.client.get(self.ENDPOINT)
- self.assertEqual(len(response.data), 1)
-
- response = self.client.post(
- self.ENDPOINT_ACKNOWLEDGE,
- {"tasks": [task.id]},
- )
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- response = self.client.get(self.ENDPOINT)
- self.assertEqual(len(response.data), 0)
-
- def test_task_result_no_error(self):
- """
- GIVEN:
- - A celery task completed without error
- WHEN:
- - API call is made to get tasks
- THEN:
- - The returned data includes the task result
- """
- PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_one.pdf",
- status=celery.states.SUCCESS,
- result="Success. New document id 1 created",
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
-
- returned_data = response.data[0]
-
- self.assertEqual(returned_data["result"], "Success. New document id 1 created")
- self.assertEqual(returned_data["related_document"], "1")
-
- def test_task_result_with_error(self):
- """
- GIVEN:
- - A celery task completed with an exception
- WHEN:
- - API call is made to get tasks
- THEN:
- - The returned result is the exception info
- """
- PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="task_one.pdf",
- status=celery.states.FAILURE,
- result="test.pdf: Not consuming test.pdf: It is a duplicate.",
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
-
- returned_data = response.data[0]
-
- self.assertEqual(
- returned_data["result"],
- "test.pdf: Not consuming test.pdf: It is a duplicate.",
- )
-
- def test_task_name_webui(self):
- """
- GIVEN:
- - Attempted celery task
- - Task was created through the webui
- WHEN:
- - API call is made to get tasks
- THEN:
- - Returned data include the filename
- """
- PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="test.pdf",
- task_name="documents.tasks.some_task",
- status=celery.states.SUCCESS,
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
-
- returned_data = response.data[0]
-
- self.assertEqual(returned_data["task_file_name"], "test.pdf")
-
- def test_task_name_consume_folder(self):
- """
- GIVEN:
- - Attempted celery task
- - Task was created through the consume folder
- WHEN:
- - API call is made to get tasks
- THEN:
- - Returned data include the filename
- """
- PaperlessTask.objects.create(
- task_id=str(uuid.uuid4()),
- task_file_name="anothertest.pdf",
- task_name="documents.tasks.some_task",
- status=celery.states.SUCCESS,
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 1)
-
- returned_data = response.data[0]
-
- self.assertEqual(returned_data["task_file_name"], "anothertest.pdf")
-
-
-class TestApiUser(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/users/"
-
- def setUp(self):
- super().setUp()
-
- self.user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=self.user)
-
- def test_get_users(self):
- """
- GIVEN:
- - Configured users
- WHEN:
- - API call is made to get users
- THEN:
- - Configured users are provided
- """
-
- user1 = User.objects.create(
- username="testuser",
- password="test",
- first_name="Test",
- last_name="User",
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 2)
- returned_user2 = response.data["results"][1]
-
- self.assertEqual(returned_user2["username"], user1.username)
- self.assertEqual(returned_user2["password"], "**********")
- self.assertEqual(returned_user2["first_name"], user1.first_name)
- self.assertEqual(returned_user2["last_name"], user1.last_name)
-
- def test_create_user(self):
- """
- WHEN:
- - API request is made to add a user account
- THEN:
- - A new user account is created
- """
-
- user1 = {
- "username": "testuser",
- "password": "test",
- "first_name": "Test",
- "last_name": "User",
- }
-
- response = self.client.post(
- self.ENDPOINT,
- data=user1,
- )
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- returned_user1 = User.objects.get(username="testuser")
-
- self.assertEqual(returned_user1.username, user1["username"])
- self.assertEqual(returned_user1.first_name, user1["first_name"])
- self.assertEqual(returned_user1.last_name, user1["last_name"])
-
- def test_delete_user(self):
- """
- GIVEN:
- - Existing user account
- WHEN:
- - API request is made to delete a user account
- THEN:
- - Account is deleted
- """
-
- user1 = User.objects.create(
- username="testuser",
- password="test",
- first_name="Test",
- last_name="User",
- )
-
- nUsers = User.objects.count()
-
- response = self.client.delete(
- f"{self.ENDPOINT}{user1.pk}/",
- )
-
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- self.assertEqual(User.objects.count(), nUsers - 1)
-
- def test_update_user(self):
- """
- GIVEN:
- - Existing user accounts
- WHEN:
- - API request is made to update user account
- THEN:
- - The user account is updated, password only updated if not '****'
- """
-
- user1 = User.objects.create(
- username="testuser",
- password="test",
- first_name="Test",
- last_name="User",
- )
-
- initial_password = user1.password
-
- response = self.client.patch(
- f"{self.ENDPOINT}{user1.pk}/",
- data={
- "first_name": "Updated Name 1",
- "password": "******",
- },
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- returned_user1 = User.objects.get(pk=user1.pk)
- self.assertEqual(returned_user1.first_name, "Updated Name 1")
- self.assertEqual(returned_user1.password, initial_password)
-
- response = self.client.patch(
- f"{self.ENDPOINT}{user1.pk}/",
- data={
- "first_name": "Updated Name 2",
- "password": "123xyz",
- },
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- returned_user2 = User.objects.get(pk=user1.pk)
- self.assertEqual(returned_user2.first_name, "Updated Name 2")
- self.assertNotEqual(returned_user2.password, initial_password)
-
-
-class TestApiGroup(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/groups/"
-
- def setUp(self):
- super().setUp()
-
- self.user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=self.user)
-
- def test_get_groups(self):
- """
- GIVEN:
- - Configured groups
- WHEN:
- - API call is made to get groups
- THEN:
- - Configured groups are provided
- """
-
- group1 = Group.objects.create(
- name="Test Group",
- )
-
- response = self.client.get(self.ENDPOINT)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 1)
- returned_group1 = response.data["results"][0]
-
- self.assertEqual(returned_group1["name"], group1.name)
-
- def test_create_group(self):
- """
- WHEN:
- - API request is made to add a group
- THEN:
- - A new group is created
- """
-
- group1 = {
- "name": "Test Group",
- }
-
- response = self.client.post(
- self.ENDPOINT,
- data=group1,
- )
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- returned_group1 = Group.objects.get(name="Test Group")
-
- self.assertEqual(returned_group1.name, group1["name"])
-
- def test_delete_group(self):
- """
- GIVEN:
- - Existing group
- WHEN:
- - API request is made to delete a group
- THEN:
- - Group is deleted
- """
-
- group1 = Group.objects.create(
- name="Test Group",
- )
-
- response = self.client.delete(
- f"{self.ENDPOINT}{group1.pk}/",
- )
-
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- self.assertEqual(len(Group.objects.all()), 0)
-
- def test_update_group(self):
- """
- GIVEN:
- - Existing groups
- WHEN:
- - API request is made to update group
- THEN:
- - The group is updated
- """
-
- group1 = Group.objects.create(
- name="Test Group",
- )
-
- response = self.client.patch(
- f"{self.ENDPOINT}{group1.pk}/",
- data={
- "name": "Updated Name 1",
- },
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- returned_group1 = Group.objects.get(pk=group1.pk)
- self.assertEqual(returned_group1.name, "Updated Name 1")
-
-
-class TestBulkEditObjectPermissions(APITestCase):
- def setUp(self):
- super().setUp()
-
- user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=user)
-
- self.t1 = Tag.objects.create(name="t1")
- self.t2 = Tag.objects.create(name="t2")
- self.c1 = Correspondent.objects.create(name="c1")
- self.dt1 = DocumentType.objects.create(name="dt1")
- self.sp1 = StoragePath.objects.create(name="sp1")
- self.user1 = User.objects.create(username="user1")
- self.user2 = User.objects.create(username="user2")
- self.user3 = User.objects.create(username="user3")
-
- def test_bulk_object_set_permissions(self):
- """
- GIVEN:
- - Existing objects
- WHEN:
- - bulk_edit_object_perms API endpoint is called
- THEN:
- - Permissions and / or owner are changed
- """
- permissions = {
- "view": {
- "users": [self.user1.id, self.user2.id],
- "groups": [],
- },
- "change": {
- "users": [self.user1.id],
- "groups": [],
- },
- }
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.t1.id, self.t2.id],
- "object_type": "tags",
- "permissions": permissions,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn(self.user1, get_users_with_perms(self.t1))
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.c1.id],
- "object_type": "correspondents",
- "permissions": permissions,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn(self.user1, get_users_with_perms(self.c1))
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.dt1.id],
- "object_type": "document_types",
- "permissions": permissions,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn(self.user1, get_users_with_perms(self.dt1))
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.sp1.id],
- "object_type": "storage_paths",
- "permissions": permissions,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn(self.user1, get_users_with_perms(self.sp1))
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.t1.id, self.t2.id],
- "object_type": "tags",
- "owner": self.user3.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.sp1.id],
- "object_type": "storage_paths",
- "owner": self.user3.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3)
-
- def test_bulk_edit_object_permissions_insufficient_perms(self):
- """
- GIVEN:
- - Objects owned by user other than logged in user
- WHEN:
- - bulk_edit_object_perms API endpoint is called
- THEN:
- - User is not able to change permissions
- """
- self.t1.owner = User.objects.get(username="temp_admin")
- self.t1.save()
- self.client.force_authenticate(user=self.user1)
-
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.t1.id, self.t2.id],
- "object_type": "tags",
- "owner": self.user1.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- self.assertEqual(response.content, b"Insufficient permissions")
-
- def test_bulk_edit_object_permissions_validation(self):
- """
- GIVEN:
- - Existing objects
- WHEN:
- - bulk_edit_object_perms API endpoint is called with invalid params
- THEN:
- - Validation fails
- """
- # not a list
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": self.t1.id,
- "object_type": "tags",
- "owner": self.user1.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- # not a list of ints
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": ["one"],
- "object_type": "tags",
- "owner": self.user1.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- # duplicates
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [self.t1.id, self.t2.id, self.t1.id],
- "object_type": "tags",
- "owner": self.user1.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- # not a valid object type
- response = self.client.post(
- "/api/bulk_edit_object_perms/",
- json.dumps(
- {
- "objects": [1],
- "object_type": "madeup",
- "owner": self.user1.id,
- },
- ),
- content_type="application/json",
- )
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
-
-class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
- ENDPOINT = "/api/consumption_templates/"
-
- def setUp(self) -> None:
- super().setUp()
-
- user = User.objects.create_superuser(username="temp_admin")
- self.client.force_authenticate(user=user)
- self.user2 = User.objects.create(username="user2")
- self.user3 = User.objects.create(username="user3")
- self.group1 = Group.objects.create(name="group1")
-
- self.c = Correspondent.objects.create(name="Correspondent Name")
- self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
- self.dt = DocumentType.objects.create(name="DocType Name")
- self.t1 = Tag.objects.create(name="t1")
- self.t2 = Tag.objects.create(name="t2")
- self.t3 = Tag.objects.create(name="t3")
- self.sp = StoragePath.objects.create(path="/test/")
- self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string")
- self.cf2 = CustomField.objects.create(
- name="Custom Field 2",
- data_type="integer",
- )
-
- self.ct = ConsumptionTemplate.objects.create(
- name="Template 1",
- order=0,
- sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
- filter_filename="*simple*",
- filter_path="*/samples/*",
- assign_title="Doc from {correspondent}",
- assign_correspondent=self.c,
- assign_document_type=self.dt,
- assign_storage_path=self.sp,
- assign_owner=self.user2,
- )
- self.ct.assign_tags.add(self.t1)
- self.ct.assign_tags.add(self.t2)
- self.ct.assign_tags.add(self.t3)
- self.ct.assign_view_users.add(self.user3.pk)
- self.ct.assign_view_groups.add(self.group1.pk)
- self.ct.assign_change_users.add(self.user3.pk)
- self.ct.assign_change_groups.add(self.group1.pk)
- self.ct.assign_custom_fields.add(self.cf1.pk)
- self.ct.assign_custom_fields.add(self.cf2.pk)
- self.ct.save()
-
- def test_api_get_consumption_template(self):
- """
- GIVEN:
- - API request to get all consumption template
- WHEN:
- - API is called
- THEN:
- - Existing consumption templates are returned
- """
- response = self.client.get(self.ENDPOINT, format="json")
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data["count"], 1)
-
- resp_consumption_template = response.data["results"][0]
- self.assertEqual(resp_consumption_template["id"], self.ct.id)
- self.assertEqual(
- resp_consumption_template["assign_correspondent"],
- self.ct.assign_correspondent.pk,
- )
-
- def test_api_create_consumption_template(self):
- """
- GIVEN:
- - API request to create a consumption template
- WHEN:
- - API is called
- THEN:
- - Correct HTTP response
- - New template is created
- """
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Template 2",
- "order": 1,
- "sources": [DocumentSource.ApiUpload],
- "filter_filename": "*test*",
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(ConsumptionTemplate.objects.count(), 2)
-
- def test_api_create_invalid_consumption_template(self):
- """
- GIVEN:
- - API request to create a consumption template
- - Neither file name nor path filter are specified
- WHEN:
- - API is called
- THEN:
- - Correct HTTP 400 response
- - No template is created
- """
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Template 2",
- "order": 1,
- "sources": [DocumentSource.ApiUpload],
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(ConsumptionTemplate.objects.count(), 1)
-
- def test_api_create_consumption_template_empty_fields(self):
- """
- GIVEN:
- - API request to create a consumption template
- - Path or filename filter or assign title are empty string
- WHEN:
- - API is called
- THEN:
- - Template is created but filter or title assignment is not set if ""
- """
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Template 2",
- "order": 1,
- "sources": [DocumentSource.ApiUpload],
- "filter_filename": "*test*",
- "filter_path": "",
- "assign_title": "",
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- ct = ConsumptionTemplate.objects.get(name="Template 2")
- self.assertEqual(ct.filter_filename, "*test*")
- self.assertIsNone(ct.filter_path)
- self.assertIsNone(ct.assign_title)
-
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Template 3",
- "order": 1,
- "sources": [DocumentSource.ApiUpload],
- "filter_filename": "",
- "filter_path": "*/test/*",
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- ct2 = ConsumptionTemplate.objects.get(name="Template 3")
- self.assertEqual(ct2.filter_path, "*/test/*")
- self.assertIsNone(ct2.filter_filename)
-
- def test_api_create_consumption_template_with_mailrule(self):
- """
- GIVEN:
- - API request to create a consumption template with a mail rule but no MailFetch source
- WHEN:
- - API is called
- THEN:
- - Correct HTTP response
- - New template is created with MailFetch as source
- """
- account1 = MailAccount.objects.create(
- name="Email1",
- username="username1",
- password="password1",
- imap_server="server.example.com",
- imap_port=443,
- imap_security=MailAccount.ImapSecurity.SSL,
- character_set="UTF-8",
- )
- rule1 = MailRule.objects.create(
- name="Rule1",
- account=account1,
- folder="INBOX",
- filter_from="from@example.com",
- filter_to="someone@somewhere.com",
- filter_subject="subject",
- filter_body="body",
- filter_attachment_filename_include="file.pdf",
- maximum_age=30,
- action=MailRule.MailAction.MARK_READ,
- assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
- assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
- order=0,
- attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
- )
- response = self.client.post(
- self.ENDPOINT,
- json.dumps(
- {
- "name": "Template 2",
- "order": 1,
- "sources": [DocumentSource.ApiUpload],
- "filter_mailrule": rule1.pk,
- },
- ),
- content_type="application/json",
- )
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(ConsumptionTemplate.objects.count(), 2)
- ct = ConsumptionTemplate.objects.get(name="Template 2")
- self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()])
--- /dev/null
+import datetime
+import io
+import json
+import os
+import shutil
+import zipfile
+
+from django.contrib.auth.models import User
+from django.test import override_settings
+from django.utils import timezone
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.models import Correspondent
+from documents.models import Document
+from documents.models import DocumentType
+from documents.tests.utils import DirectoriesMixin
+
+
+class TestBulkDownload(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/documents/bulk_download/"
+
+ def setUp(self):
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+
+ self.doc1 = Document.objects.create(title="unrelated", checksum="A")
+ self.doc2 = Document.objects.create(
+ title="document A",
+ filename="docA.pdf",
+ mime_type="application/pdf",
+ checksum="B",
+ created=timezone.make_aware(datetime.datetime(2021, 1, 1)),
+ )
+ self.doc2b = Document.objects.create(
+ title="document A",
+ filename="docA2.pdf",
+ mime_type="application/pdf",
+ checksum="D",
+ created=timezone.make_aware(datetime.datetime(2021, 1, 1)),
+ )
+ self.doc3 = Document.objects.create(
+ title="document B",
+ filename="docB.jpg",
+ mime_type="image/jpeg",
+ checksum="C",
+ created=timezone.make_aware(datetime.datetime(2020, 3, 21)),
+ archive_filename="docB.pdf",
+ archive_checksum="D",
+ )
+
+ shutil.copy(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ self.doc2.source_path,
+ )
+ shutil.copy(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.png"),
+ self.doc2b.source_path,
+ )
+ shutil.copy(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"),
+ self.doc3.source_path,
+ )
+ shutil.copy(
+ os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"),
+ self.doc3.archive_path,
+ )
+
+ def test_download_originals(self):
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {"documents": [self.doc2.id, self.doc3.id], "content": "originals"},
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 2)
+ self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
+ self.assertIn("2020-03-21 document B.jpg", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
+
+ with self.doc3.source_file as f:
+ self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg"))
+
+ def test_download_default(self):
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps({"documents": [self.doc2.id, self.doc3.id]}),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 2)
+ self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
+ self.assertIn("2020-03-21 document B.pdf", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
+
+ with self.doc3.archive_file as f:
+ self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf"))
+
+ def test_download_both(self):
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps({"documents": [self.doc2.id, self.doc3.id], "content": "both"}),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 3)
+ self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist())
+ self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist())
+ self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("originals/2021-01-01 document A.pdf"),
+ )
+
+ with self.doc3.archive_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("archive/2020-03-21 document B.pdf"),
+ )
+
+ with self.doc3.source_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("originals/2020-03-21 document B.jpg"),
+ )
+
+ def test_filename_clashes(self):
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps({"documents": [self.doc2.id, self.doc2b.id]}),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 2)
+
+ self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
+ self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
+
+ with self.doc2b.source_file as f:
+ self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf"))
+
+ def test_compression(self):
+ self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {"documents": [self.doc2.id, self.doc2b.id], "compression": "lzma"},
+ ),
+ content_type="application/json",
+ )
+
+ @override_settings(FILENAME_FORMAT="{correspondent}/{title}")
+ def test_formatted_download_originals(self):
+ """
+ GIVEN:
+ - Defined file naming format
+ WHEN:
+ - Bulk download request for original documents
+ - Bulk download request requests to follow format
+ THEN:
+ - Files in resulting zipfile are formatted
+ """
+
+ c = Correspondent.objects.create(name="test")
+ c2 = Correspondent.objects.create(name="a space name")
+
+ self.doc2.correspondent = c
+ self.doc2.title = "This is Doc 2"
+ self.doc2.save()
+
+ self.doc3.correspondent = c2
+ self.doc3.title = "Title 2 - Doc 3"
+ self.doc3.save()
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc2.id, self.doc3.id],
+ "content": "originals",
+ "follow_formatting": True,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 2)
+ self.assertIn("a space name/Title 2 - Doc 3.jpg", zipf.namelist())
+ self.assertIn("test/This is Doc 2.pdf", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(f.read(), zipf.read("test/This is Doc 2.pdf"))
+
+ with self.doc3.source_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("a space name/Title 2 - Doc 3.jpg"),
+ )
+
+ @override_settings(FILENAME_FORMAT="somewhere/{title}")
+ def test_formatted_download_archive(self):
+ """
+ GIVEN:
+ - Defined file naming format
+ WHEN:
+ - Bulk download request for archive documents
+ - Bulk download request requests to follow format
+ THEN:
+ - Files in resulting zipfile are formatted
+ """
+
+ self.doc2.title = "This is Doc 2"
+ self.doc2.save()
+
+ self.doc3.title = "Title 2 - Doc 3"
+ self.doc3.save()
+ print(self.doc3.archive_path)
+ print(self.doc3.archive_filename)
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc2.id, self.doc3.id],
+ "follow_formatting": True,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 2)
+ self.assertIn("somewhere/This is Doc 2.pdf", zipf.namelist())
+ self.assertIn("somewhere/Title 2 - Doc 3.pdf", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(f.read(), zipf.read("somewhere/This is Doc 2.pdf"))
+
+ with self.doc3.archive_file as f:
+ self.assertEqual(f.read(), zipf.read("somewhere/Title 2 - Doc 3.pdf"))
+
+ @override_settings(FILENAME_FORMAT="{document_type}/{title}")
+ def test_formatted_download_both(self):
+ """
+ GIVEN:
+ - Defined file naming format
+ WHEN:
+ - Bulk download request for original documents and archive documents
+ - Bulk download request requests to follow format
+ THEN:
+ - Files defined in resulting zipfile are formatted
+ """
+
+ dc1 = DocumentType.objects.create(name="bill")
+ dc2 = DocumentType.objects.create(name="statement")
+
+ self.doc2.document_type = dc1
+ self.doc2.title = "This is Doc 2"
+ self.doc2.save()
+
+ self.doc3.document_type = dc2
+ self.doc3.title = "Title 2 - Doc 3"
+ self.doc3.save()
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc2.id, self.doc3.id],
+ "content": "both",
+ "follow_formatting": True,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response["Content-Type"], "application/zip")
+
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
+ self.assertEqual(len(zipf.filelist), 3)
+ self.assertIn("originals/bill/This is Doc 2.pdf", zipf.namelist())
+ self.assertIn("archive/statement/Title 2 - Doc 3.pdf", zipf.namelist())
+ self.assertIn("originals/statement/Title 2 - Doc 3.jpg", zipf.namelist())
+
+ with self.doc2.source_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("originals/bill/This is Doc 2.pdf"),
+ )
+
+ with self.doc3.archive_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("archive/statement/Title 2 - Doc 3.pdf"),
+ )
+
+ with self.doc3.source_file as f:
+ self.assertEqual(
+ f.read(),
+ zipf.read("originals/statement/Title 2 - Doc 3.jpg"),
+ )
--- /dev/null
+import json
+from unittest import mock
+
+from django.contrib.auth.models import User
+from guardian.shortcuts import assign_perm
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents import bulk_edit
+from documents.models import Correspondent
+from documents.models import Document
+from documents.models import DocumentType
+from documents.models import StoragePath
+from documents.models import Tag
+from documents.tests.utils import DirectoriesMixin
+
+
+class TestBulkEdit(DirectoriesMixin, APITestCase):
+ def setUp(self):
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+
+ patcher = mock.patch("documents.bulk_edit.bulk_update_documents.delay")
+ self.async_task = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.c1 = Correspondent.objects.create(name="c1")
+ self.c2 = Correspondent.objects.create(name="c2")
+ self.dt1 = DocumentType.objects.create(name="dt1")
+ self.dt2 = DocumentType.objects.create(name="dt2")
+ self.t1 = Tag.objects.create(name="t1")
+ self.t2 = Tag.objects.create(name="t2")
+ self.doc1 = Document.objects.create(checksum="A", title="A")
+ self.doc2 = Document.objects.create(
+ checksum="B",
+ title="B",
+ correspondent=self.c1,
+ document_type=self.dt1,
+ )
+ self.doc3 = Document.objects.create(
+ checksum="C",
+ title="C",
+ correspondent=self.c2,
+ document_type=self.dt2,
+ )
+ self.doc4 = Document.objects.create(checksum="D", title="D")
+ self.doc5 = Document.objects.create(checksum="E", title="E")
+ self.doc2.tags.add(self.t1)
+ self.doc3.tags.add(self.t2)
+ self.doc4.tags.add(self.t1, self.t2)
+ self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
+
+ def test_set_correspondent(self):
+ self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
+ bulk_edit.set_correspondent(
+ [self.doc1.id, self.doc2.id, self.doc3.id],
+ self.c2.id,
+ )
+ self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3)
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
+
+ def test_unset_correspondent(self):
+ self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
+ bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None)
+ self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0)
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
+
+ def test_set_document_type(self):
+ self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
+ bulk_edit.set_document_type(
+ [self.doc1.id, self.doc2.id, self.doc3.id],
+ self.dt2.id,
+ )
+ self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3)
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id])
+
+ def test_unset_document_type(self):
+ self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
+ bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None)
+ self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0)
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
+
+ def test_set_document_storage_path(self):
+ """
+ GIVEN:
+ - 5 documents without defined storage path
+ WHEN:
+ - Bulk edit called to add storage path to 1 document
+ THEN:
+ - Single document storage path update
+ """
+ self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
+
+ bulk_edit.set_storage_path(
+ [self.doc1.id],
+ self.sp1.id,
+ )
+
+ self.assertEqual(Document.objects.filter(storage_path=None).count(), 4)
+
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+
+ self.assertCountEqual(kwargs["document_ids"], [self.doc1.id])
+
+ def test_unset_document_storage_path(self):
+ """
+ GIVEN:
+ - 4 documents without defined storage path
+ - 1 document with a defined storage
+ WHEN:
+ - Bulk edit called to remove storage path from 1 document
+ THEN:
+ - Single document storage path removed
+ """
+ self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
+
+ bulk_edit.set_storage_path(
+ [self.doc1.id],
+ self.sp1.id,
+ )
+
+ self.assertEqual(Document.objects.filter(storage_path=None).count(), 4)
+
+ bulk_edit.set_storage_path(
+ [self.doc1.id],
+ None,
+ )
+
+ self.assertEqual(Document.objects.filter(storage_path=None).count(), 5)
+
+ self.async_task.assert_called()
+ args, kwargs = self.async_task.call_args
+
+ self.assertCountEqual(kwargs["document_ids"], [self.doc1.id])
+
+ def test_add_tag(self):
+ self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
+ bulk_edit.add_tag(
+ [self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id],
+ self.t1.id,
+ )
+ self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4)
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc3.id])
+
+ def test_remove_tag(self):
+ self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
+ bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id)
+ self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1)
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs["document_ids"], [self.doc4.id])
+
+ def test_modify_tags(self):
+ tag_unrelated = Tag.objects.create(name="unrelated")
+ self.doc2.tags.add(tag_unrelated)
+ self.doc3.tags.add(tag_unrelated)
+ bulk_edit.modify_tags(
+ [self.doc2.id, self.doc3.id],
+ add_tags=[self.t2.id],
+ remove_tags=[self.t1.id],
+ )
+
+ self.assertCountEqual(list(self.doc2.tags.all()), [self.t2, tag_unrelated])
+ self.assertCountEqual(list(self.doc3.tags.all()), [self.t2, tag_unrelated])
+
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ # TODO: doc3 should not be affected, but the query for that is rather complicated
+ self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
+
+ def test_delete(self):
+ self.assertEqual(Document.objects.count(), 5)
+ bulk_edit.delete([self.doc1.id, self.doc2.id])
+ self.assertEqual(Document.objects.count(), 3)
+ self.assertCountEqual(
+ [doc.id for doc in Document.objects.all()],
+ [self.doc3.id, self.doc4.id, self.doc5.id],
+ )
+
+ @mock.patch("documents.serialisers.bulk_edit.set_correspondent")
+ def test_api_set_correspondent(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_correspondent",
+ "parameters": {"correspondent": self.c1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertEqual(kwargs["correspondent"], self.c1.id)
+
+ @mock.patch("documents.serialisers.bulk_edit.set_correspondent")
+ def test_api_unset_correspondent(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_correspondent",
+ "parameters": {"correspondent": None},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertIsNone(kwargs["correspondent"])
+
+ @mock.patch("documents.serialisers.bulk_edit.set_document_type")
+ def test_api_set_type(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_document_type",
+ "parameters": {"document_type": self.dt1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertEqual(kwargs["document_type"], self.dt1.id)
+
+ @mock.patch("documents.serialisers.bulk_edit.set_document_type")
+ def test_api_unset_type(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_document_type",
+ "parameters": {"document_type": None},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertIsNone(kwargs["document_type"])
+
+ @mock.patch("documents.serialisers.bulk_edit.add_tag")
+ def test_api_add_tag(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "add_tag",
+ "parameters": {"tag": self.t1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertEqual(kwargs["tag"], self.t1.id)
+
+ @mock.patch("documents.serialisers.bulk_edit.remove_tag")
+ def test_api_remove_tag(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "remove_tag",
+ "parameters": {"tag": self.t1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertEqual(kwargs["tag"], self.t1.id)
+
+ @mock.patch("documents.serialisers.bulk_edit.modify_tags")
+ def test_api_modify_tags(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id, self.doc3.id],
+ "method": "modify_tags",
+ "parameters": {
+ "add_tags": [self.t1.id],
+ "remove_tags": [self.t2.id],
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertListEqual(args[0], [self.doc1.id, self.doc3.id])
+ self.assertEqual(kwargs["add_tags"], [self.t1.id])
+ self.assertEqual(kwargs["remove_tags"], [self.t2.id])
+
+ @mock.patch("documents.serialisers.bulk_edit.modify_tags")
+ def test_api_modify_tags_not_provided(self, m):
+ """
+ GIVEN:
+ - API data to modify tags is missing modify_tags field
+ WHEN:
+ - API to edit tags is called
+ THEN:
+ - API returns HTTP 400
+ - modify_tags is not called
+ """
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id, self.doc3.id],
+ "method": "modify_tags",
+ "parameters": {
+ "add_tags": [self.t1.id],
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ m.assert_not_called()
+
+ @mock.patch("documents.serialisers.bulk_edit.delete")
+ def test_api_delete(self, m):
+ m.return_value = "OK"
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {"documents": [self.doc1.id], "method": "delete", "parameters": {}},
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertEqual(args[0], [self.doc1.id])
+ self.assertEqual(len(kwargs), 0)
+
+ @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
+ def test_api_set_storage_path(self, m):
+ """
+ GIVEN:
+ - API data to set the storage path of a document
+ WHEN:
+ - API is called
+ THEN:
+ - set_storage_path is called with correct document IDs and storage_path ID
+ """
+ m.return_value = "OK"
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_storage_path",
+ "parameters": {"storage_path": self.sp1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+
+ self.assertListEqual(args[0], [self.doc1.id])
+ self.assertEqual(kwargs["storage_path"], self.sp1.id)
+
+ @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
+ def test_api_unset_storage_path(self, m):
+ """
+ GIVEN:
+ - API data to clear/unset the storage path of a document
+ WHEN:
+ - API is called
+ THEN:
+ - set_storage_path is called with correct document IDs and None storage_path
+ """
+ m.return_value = "OK"
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_storage_path",
+ "parameters": {"storage_path": None},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+ args, kwargs = m.call_args
+
+ self.assertListEqual(args[0], [self.doc1.id])
+ self.assertEqual(kwargs["storage_path"], None)
+
+ def test_api_invalid_storage_path(self):
+ """
+ GIVEN:
+ - API data to set the storage path of a document
+ - Given storage_path ID isn't valid
+ WHEN:
+ - API is called
+ THEN:
+ - set_storage_path is called with correct document IDs and storage_path ID
+ """
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_storage_path",
+ "parameters": {"storage_path": self.sp1.id + 10},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.async_task.assert_not_called()
+
+ def test_api_set_storage_path_not_provided(self):
+ """
+ GIVEN:
+ - API data to set the storage path of a document
+ - API data is missing storage path ID
+ WHEN:
+ - API is called
+ THEN:
+ - set_storage_path is called with correct document IDs and storage_path ID
+ """
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id],
+ "method": "set_storage_path",
+ "parameters": {},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.async_task.assert_not_called()
+
+ def test_api_invalid_doc(self):
+ self.assertEqual(Document.objects.count(), 5)
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps({"documents": [-235], "method": "delete", "parameters": {}}),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(Document.objects.count(), 5)
+
+ def test_api_invalid_method(self):
+ self.assertEqual(Document.objects.count(), 5)
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "exterminate",
+ "parameters": {},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(Document.objects.count(), 5)
+
+ def test_api_invalid_correspondent(self):
+ self.assertEqual(self.doc2.correspondent, self.c1)
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "set_correspondent",
+ "parameters": {"correspondent": 345657},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ doc2 = Document.objects.get(id=self.doc2.id)
+ self.assertEqual(doc2.correspondent, self.c1)
+
+ def test_api_no_correspondent(self):
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "set_correspondent",
+ "parameters": {},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_api_invalid_document_type(self):
+ self.assertEqual(self.doc2.document_type, self.dt1)
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "set_document_type",
+ "parameters": {"document_type": 345657},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ doc2 = Document.objects.get(id=self.doc2.id)
+ self.assertEqual(doc2.document_type, self.dt1)
+
+ def test_api_no_document_type(self):
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "set_document_type",
+ "parameters": {},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_api_add_invalid_tag(self):
+ self.assertEqual(list(self.doc2.tags.all()), [self.t1])
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "add_tag",
+ "parameters": {"tag": 345657},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ self.assertEqual(list(self.doc2.tags.all()), [self.t1])
+
+ def test_api_add_tag_no_tag(self):
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {"documents": [self.doc2.id], "method": "add_tag", "parameters": {}},
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_api_delete_invalid_tag(self):
+ self.assertEqual(list(self.doc2.tags.all()), [self.t1])
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "remove_tag",
+ "parameters": {"tag": 345657},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ self.assertEqual(list(self.doc2.tags.all()), [self.t1])
+
+ def test_api_delete_tag_no_tag(self):
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {"documents": [self.doc2.id], "method": "remove_tag", "parameters": {}},
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_api_modify_invalid_tags(self):
+ self.assertEqual(list(self.doc2.tags.all()), [self.t1])
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "modify_tags",
+ "parameters": {
+ "add_tags": [self.t2.id, 1657],
+ "remove_tags": [1123123],
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_api_modify_tags_no_tags(self):
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "modify_tags",
+ "parameters": {"remove_tags": [1123123]},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id],
+ "method": "modify_tags",
+ "parameters": {"add_tags": [self.t2.id, 1657]},
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_api_selection_data_empty(self):
+ response = self.client.post(
+ "/api/documents/selection_data/",
+ json.dumps({"documents": []}),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ for field, Entity in [
+ ("selected_correspondents", Correspondent),
+ ("selected_tags", Tag),
+ ("selected_document_types", DocumentType),
+ ]:
+ self.assertEqual(len(response.data[field]), Entity.objects.count())
+ for correspondent in response.data[field]:
+ self.assertEqual(correspondent["document_count"], 0)
+ self.assertCountEqual(
+ map(lambda c: c["id"], response.data[field]),
+ map(lambda c: c["id"], Entity.objects.values("id")),
+ )
+
+ def test_api_selection_data(self):
+ response = self.client.post(
+ "/api/documents/selection_data/",
+ json.dumps(
+ {"documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id]},
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.assertCountEqual(
+ response.data["selected_correspondents"],
+ [
+ {"id": self.c1.id, "document_count": 1},
+ {"id": self.c2.id, "document_count": 0},
+ ],
+ )
+ self.assertCountEqual(
+ response.data["selected_tags"],
+ [
+ {"id": self.t1.id, "document_count": 2},
+ {"id": self.t2.id, "document_count": 1},
+ ],
+ )
+ self.assertCountEqual(
+ response.data["selected_document_types"],
+ [
+ {"id": self.c1.id, "document_count": 1},
+ {"id": self.c2.id, "document_count": 0},
+ ],
+ )
+
+ @mock.patch("documents.serialisers.bulk_edit.set_permissions")
+ def test_set_permissions(self, m):
+ m.return_value = "OK"
+ user1 = User.objects.create(username="user1")
+ user2 = User.objects.create(username="user2")
+ permissions = {
+ "view": {
+ "users": [user1.id, user2.id],
+ "groups": None,
+ },
+ "change": {
+ "users": [user1.id],
+ "groups": None,
+ },
+ }
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id, self.doc3.id],
+ "method": "set_permissions",
+ "parameters": {"set_permissions": permissions},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ m.assert_called_once()
+ args, kwargs = m.call_args
+ self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
+ self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
+
+ @mock.patch("documents.serialisers.bulk_edit.set_permissions")
+ def test_insufficient_permissions_ownership(self, m):
+ """
+ GIVEN:
+ - Documents owned by user other than logged in user
+ WHEN:
+ - set_permissions bulk edit API endpoint is called
+ THEN:
+ - User is not able to change permissions
+ """
+ m.return_value = "OK"
+ self.doc1.owner = User.objects.get(username="temp_admin")
+ self.doc1.save()
+ user1 = User.objects.create(username="user1")
+ self.client.force_authenticate(user=user1)
+
+ permissions = {
+ "owner": user1.id,
+ }
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
+ "method": "set_permissions",
+ "parameters": {"set_permissions": permissions},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ m.assert_not_called()
+ self.assertEqual(response.content, b"Insufficient permissions")
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc2.id, self.doc3.id],
+ "method": "set_permissions",
+ "parameters": {"set_permissions": permissions},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ m.assert_called_once()
+
+ @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
+ def test_insufficient_permissions_edit(self, m):
+ """
+ GIVEN:
+ - Documents for which current user only has view permissions
+ WHEN:
+ - API is called
+ THEN:
+ - set_storage_path only called if user can edit all docs
+ """
+ m.return_value = "OK"
+ self.doc1.owner = User.objects.get(username="temp_admin")
+ self.doc1.save()
+ user1 = User.objects.create(username="user1")
+ assign_perm("view_document", user1, self.doc1)
+ self.client.force_authenticate(user=user1)
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
+ "method": "set_storage_path",
+ "parameters": {"storage_path": self.sp1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ m.assert_not_called()
+ self.assertEqual(response.content, b"Insufficient permissions")
+
+ assign_perm("change_document", user1, self.doc1)
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
+ "method": "set_storage_path",
+ "parameters": {"storage_path": self.sp1.id},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ m.assert_called_once()
--- /dev/null
+import json
+
+from django.contrib.auth.models import Group
+from django.contrib.auth.models import User
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.data_models import DocumentSource
+from documents.models import ConsumptionTemplate
+from documents.models import Correspondent
+from documents.models import CustomField
+from documents.models import DocumentType
+from documents.models import StoragePath
+from documents.models import Tag
+from documents.tests.utils import DirectoriesMixin
+from paperless_mail.models import MailAccount
+from paperless_mail.models import MailRule
+
+
+class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/consumption_templates/"
+
+ def setUp(self) -> None:
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+ self.user2 = User.objects.create(username="user2")
+ self.user3 = User.objects.create(username="user3")
+ self.group1 = Group.objects.create(name="group1")
+
+ self.c = Correspondent.objects.create(name="Correspondent Name")
+ self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
+ self.dt = DocumentType.objects.create(name="DocType Name")
+ self.t1 = Tag.objects.create(name="t1")
+ self.t2 = Tag.objects.create(name="t2")
+ self.t3 = Tag.objects.create(name="t3")
+ self.sp = StoragePath.objects.create(path="/test/")
+ self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string")
+ self.cf2 = CustomField.objects.create(
+ name="Custom Field 2",
+ data_type="integer",
+ )
+
+ self.ct = ConsumptionTemplate.objects.create(
+ name="Template 1",
+ order=0,
+ sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
+ filter_filename="*simple*",
+ filter_path="*/samples/*",
+ assign_title="Doc from {correspondent}",
+ assign_correspondent=self.c,
+ assign_document_type=self.dt,
+ assign_storage_path=self.sp,
+ assign_owner=self.user2,
+ )
+ self.ct.assign_tags.add(self.t1)
+ self.ct.assign_tags.add(self.t2)
+ self.ct.assign_tags.add(self.t3)
+ self.ct.assign_view_users.add(self.user3.pk)
+ self.ct.assign_view_groups.add(self.group1.pk)
+ self.ct.assign_change_users.add(self.user3.pk)
+ self.ct.assign_change_groups.add(self.group1.pk)
+ self.ct.assign_custom_fields.add(self.cf1.pk)
+ self.ct.assign_custom_fields.add(self.cf2.pk)
+ self.ct.save()
+
+ def test_api_get_consumption_template(self):
+ """
+ GIVEN:
+ - API request to get all consumption template
+ WHEN:
+ - API is called
+ THEN:
+ - Existing consumption templates are returned
+ """
+ response = self.client.get(self.ENDPOINT, format="json")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 1)
+
+ resp_consumption_template = response.data["results"][0]
+ self.assertEqual(resp_consumption_template["id"], self.ct.id)
+ self.assertEqual(
+ resp_consumption_template["assign_correspondent"],
+ self.ct.assign_correspondent.pk,
+ )
+
+ def test_api_create_consumption_template(self):
+ """
+ GIVEN:
+ - API request to create a consumption template
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ - New template is created
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Template 2",
+ "order": 1,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*test*",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(ConsumptionTemplate.objects.count(), 2)
+
+ def test_api_create_invalid_consumption_template(self):
+ """
+ GIVEN:
+ - API request to create a consumption template
+ - Neither file name nor path filter are specified
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP 400 response
+ - No template is created
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Template 2",
+ "order": 1,
+ "sources": [DocumentSource.ApiUpload],
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(ConsumptionTemplate.objects.count(), 1)
+
+ def test_api_create_consumption_template_empty_fields(self):
+ """
+ GIVEN:
+ - API request to create a consumption template
+ - Path or filename filter or assign title are empty string
+ WHEN:
+ - API is called
+ THEN:
+ - Template is created but filter or title assignment is not set if ""
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Template 2",
+ "order": 1,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*test*",
+ "filter_path": "",
+ "assign_title": "",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ ct = ConsumptionTemplate.objects.get(name="Template 2")
+ self.assertEqual(ct.filter_filename, "*test*")
+ self.assertIsNone(ct.filter_path)
+ self.assertIsNone(ct.assign_title)
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Template 3",
+ "order": 1,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "",
+ "filter_path": "*/test/*",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ ct2 = ConsumptionTemplate.objects.get(name="Template 3")
+ self.assertEqual(ct2.filter_path, "*/test/*")
+ self.assertIsNone(ct2.filter_filename)
+
+ def test_api_create_consumption_template_with_mailrule(self):
+ """
+ GIVEN:
+ - API request to create a consumption template with a mail rule but no MailFetch source
+ WHEN:
+ - API is called
+ THEN:
+ - New template is created with MailFetch as source
+ """
+ account1 = MailAccount.objects.create(
+ name="Email1",
+ username="username1",
+ password="password1",
+ imap_server="server.example.com",
+ imap_port=443,
+ imap_security=MailAccount.ImapSecurity.SSL,
+ character_set="UTF-8",
+ )
+ rule1 = MailRule.objects.create(
+ name="Rule1",
+ account=account1,
+ folder="INBOX",
+ filter_from="from@example.com",
+ filter_to="someone@somewhere.com",
+ filter_subject="subject",
+ filter_body="body",
+ filter_attachment_filename_include="file.pdf",
+ maximum_age=30,
+ action=MailRule.MailAction.MARK_READ,
+ assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
+ assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
+ order=0,
+ attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
+ )
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Template 2",
+ "order": 1,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_mailrule": rule1.pk,
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(ConsumptionTemplate.objects.count(), 2)
+ ct = ConsumptionTemplate.objects.get(name="Template 2")
+ self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()])
--- /dev/null
+import datetime
+import os
+import shutil
+import tempfile
+import uuid
+import zoneinfo
+from datetime import timedelta
+from pathlib import Path
+from unittest import mock
+
+import celery
+from dateutil import parser
+from django.conf import settings
+from django.contrib.auth.models import Permission
+from django.contrib.auth.models import User
+from django.test import override_settings
+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 Correspondent
+from documents.models import CustomField
+from documents.models import CustomFieldInstance
+from documents.models import Document
+from documents.models import DocumentType
+from documents.models import MatchingModel
+from documents.models import Note
+from documents.models import SavedView
+from documents.models import ShareLink
+from documents.models import StoragePath
+from documents.models import Tag
+from documents.tests.utils import DirectoriesMixin
+from documents.tests.utils import DocumentConsumeDelayMixin
+
+
+class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=self.user)
+
+ def testDocuments(self):
+ response = self.client.get("/api/documents/").data
+
+ self.assertEqual(response["count"], 0)
+
+ c = Correspondent.objects.create(name="c", pk=41)
+ dt = DocumentType.objects.create(name="dt", pk=63)
+ tag = Tag.objects.create(name="t", pk=85)
+
+ doc = Document.objects.create(
+ title="WOW",
+ content="the content",
+ correspondent=c,
+ document_type=dt,
+ checksum="123",
+ mime_type="application/pdf",
+ )
+
+ doc.tags.add(tag)
+
+ response = self.client.get("/api/documents/", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 1)
+
+ returned_doc = response.data["results"][0]
+ self.assertEqual(returned_doc["id"], doc.id)
+ self.assertEqual(returned_doc["title"], doc.title)
+ self.assertEqual(returned_doc["correspondent"], c.id)
+ self.assertEqual(returned_doc["document_type"], dt.id)
+ self.assertListEqual(returned_doc["tags"], [tag.id])
+
+ c2 = Correspondent.objects.create(name="c2")
+
+ returned_doc["correspondent"] = c2.pk
+ returned_doc["title"] = "the new title"
+
+ response = self.client.put(
+ f"/api/documents/{doc.pk}/",
+ returned_doc,
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ doc_after_save = Document.objects.get(id=doc.id)
+
+ self.assertEqual(doc_after_save.correspondent, c2)
+ self.assertEqual(doc_after_save.title, "the new title")
+
+ self.client.delete(f"/api/documents/{doc_after_save.pk}/")
+
+ self.assertEqual(len(Document.objects.all()), 0)
+
+ def test_document_fields(self):
+ c = Correspondent.objects.create(name="c", pk=41)
+ dt = DocumentType.objects.create(name="dt", pk=63)
+ Tag.objects.create(name="t", pk=85)
+ storage_path = StoragePath.objects.create(name="sp", pk=77, path="p")
+ Document.objects.create(
+ title="WOW",
+ content="the content",
+ correspondent=c,
+ document_type=dt,
+ checksum="123",
+ mime_type="application/pdf",
+ storage_path=storage_path,
+ )
+
+ response = self.client.get("/api/documents/", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results_full = response.data["results"]
+ self.assertIn("content", results_full[0])
+ self.assertIn("id", results_full[0])
+
+ response = self.client.get("/api/documents/?fields=id", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertFalse("content" in results[0])
+ self.assertIn("id", results[0])
+ self.assertEqual(len(results[0]), 1)
+
+ response = self.client.get("/api/documents/?fields=content", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertIn("content", results[0])
+ self.assertFalse("id" in results[0])
+ self.assertEqual(len(results[0]), 1)
+
+ response = self.client.get("/api/documents/?fields=id,content", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertIn("content", results[0])
+ self.assertIn("id", results[0])
+ self.assertEqual(len(results[0]), 2)
+
+ response = self.client.get(
+ "/api/documents/?fields=id,conteasdnt",
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertFalse("content" in results[0])
+ self.assertIn("id", results[0])
+ self.assertEqual(len(results[0]), 1)
+
+ response = self.client.get("/api/documents/?fields=", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results_full[0]), len(results[0]))
+
+ response = self.client.get("/api/documents/?fields=dgfhs", format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results[0]), 0)
+
+ def test_document_actions(self):
+ _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
+
+ content = b"This is a test"
+ content_thumbnail = b"thumbnail content"
+
+ with open(filename, "wb") as f:
+ f.write(content)
+
+ doc = Document.objects.create(
+ title="none",
+ filename=os.path.basename(filename),
+ mime_type="application/pdf",
+ )
+
+ with open(
+ os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"),
+ "wb",
+ ) as f:
+ f.write(content_thumbnail)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/download/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/preview/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content_thumbnail)
+
+ def test_document_actions_with_perms(self):
+ """
+ GIVEN:
+ - Document with owner and without granted permissions
+ - User is then granted permissions
+ WHEN:
+ - User tries to load preview, thumbnail
+ THEN:
+ - Initially, HTTP 403 Forbidden
+ - With permissions, HTTP 200 OK
+ """
+ _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
+
+ content = b"This is a test"
+ content_thumbnail = b"thumbnail content"
+
+ with open(filename, "wb") as f:
+ f.write(content)
+
+ user1 = User.objects.create_user(username="test1")
+ user2 = User.objects.create_user(username="test2")
+ user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+ user2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+
+ self.client.force_authenticate(user2)
+
+ doc = Document.objects.create(
+ title="none",
+ filename=os.path.basename(filename),
+ mime_type="application/pdf",
+ owner=user1,
+ )
+
+ with open(
+ os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"),
+ "wb",
+ ) as f:
+ f.write(content_thumbnail)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/download/")
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/preview/")
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ assign_perm("view_document", user2, doc)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/download/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/preview/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ @override_settings(FILENAME_FORMAT="")
+ def test_download_with_archive(self):
+ content = b"This is a test"
+ content_archive = b"This is the same test but archived"
+
+ doc = Document.objects.create(
+ title="none",
+ filename="my_document.pdf",
+ archive_filename="archived.pdf",
+ mime_type="application/pdf",
+ )
+
+ with open(doc.source_path, "wb") as f:
+ f.write(content)
+
+ with open(doc.archive_path, "wb") as f:
+ f.write(content_archive)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/download/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content_archive)
+
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/download/?original=true",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/preview/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content_archive)
+
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/preview/?original=true",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.content, content)
+
+ def test_document_actions_not_existing_file(self):
+ doc = Document.objects.create(
+ title="none",
+ filename=os.path.basename("asd"),
+ mime_type="application/pdf",
+ )
+
+ response = self.client.get(f"/api/documents/{doc.pk}/download/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/preview/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_document_filters(self):
+ doc1 = Document.objects.create(
+ title="none1",
+ checksum="A",
+ mime_type="application/pdf",
+ )
+ doc2 = Document.objects.create(
+ title="none2",
+ checksum="B",
+ mime_type="application/pdf",
+ )
+ doc3 = Document.objects.create(
+ title="none3",
+ checksum="C",
+ mime_type="application/pdf",
+ )
+
+ tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
+ tag_2 = Tag.objects.create(name="t2")
+ tag_3 = Tag.objects.create(name="t3")
+
+ cf1 = CustomField.objects.create(
+ name="stringfield",
+ data_type=CustomField.FieldDataType.STRING,
+ )
+ cf2 = CustomField.objects.create(
+ name="numberfield",
+ data_type=CustomField.FieldDataType.INT,
+ )
+
+ doc1.tags.add(tag_inbox)
+ doc2.tags.add(tag_2)
+ doc3.tags.add(tag_2)
+ doc3.tags.add(tag_3)
+
+ cf1_d1 = CustomFieldInstance.objects.create(
+ document=doc1,
+ field=cf1,
+ value_text="foobard1",
+ )
+ CustomFieldInstance.objects.create(
+ document=doc1,
+ field=cf2,
+ value_int=999,
+ )
+ cf1_d3 = CustomFieldInstance.objects.create(
+ document=doc3,
+ field=cf1,
+ value_text="foobard3",
+ )
+
+ response = self.client.get("/api/documents/?is_in_inbox=true")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc1.id)
+
+ response = self.client.get("/api/documents/?is_in_inbox=false")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+ self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id])
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__in={tag_inbox.id},{tag_3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+ self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc3.id])
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__in={tag_2.id},{tag_3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+ self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id])
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__all={tag_2.id},{tag_3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc3.id)
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__all={tag_inbox.id},{tag_3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 0)
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__all={tag_inbox.id}a{tag_3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 3)
+
+ response = self.client.get(f"/api/documents/?tags__id__none={tag_3.id}")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+ self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc2.id])
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__none={tag_3.id},{tag_2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc1.id)
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__none={tag_2.id},{tag_inbox.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 0)
+
+ response = self.client.get(
+ f"/api/documents/?id__in={doc1.id},{doc2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+
+ response = self.client.get(
+ f"/api/documents/?id__range={doc1.id},{doc3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 3)
+
+ response = self.client.get(
+ f"/api/documents/?id={doc2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+
+ # custom field name
+ response = self.client.get(
+ f"/api/documents/?custom_fields__icontains={cf1.name}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+
+ # custom field value
+ response = self.client.get(
+ f"/api/documents/?custom_fields__icontains={cf1_d1.value}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc1.id)
+
+ response = self.client.get(
+ f"/api/documents/?custom_fields__icontains={cf1_d3.value}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc3.id)
+
+ def test_document_checksum_filter(self):
+ Document.objects.create(
+ title="none1",
+ checksum="A",
+ mime_type="application/pdf",
+ )
+ doc2 = Document.objects.create(
+ title="none2",
+ checksum="B",
+ mime_type="application/pdf",
+ )
+ Document.objects.create(
+ title="none3",
+ checksum="C",
+ mime_type="application/pdf",
+ )
+
+ response = self.client.get("/api/documents/?checksum__iexact=B")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc2.id)
+
+ response = self.client.get("/api/documents/?checksum__iexact=X")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 0)
+
+ def test_document_original_filename_filter(self):
+ doc1 = Document.objects.create(
+ title="none1",
+ checksum="A",
+ mime_type="application/pdf",
+ original_filename="docA.pdf",
+ )
+ doc2 = Document.objects.create(
+ title="none2",
+ checksum="B",
+ mime_type="application/pdf",
+ original_filename="docB.pdf",
+ )
+ doc3 = Document.objects.create(
+ title="none3",
+ checksum="C",
+ mime_type="application/pdf",
+ original_filename="docC.pdf",
+ )
+
+ response = self.client.get("/api/documents/?original_filename__iexact=DOCa.pdf")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], doc1.id)
+
+ response = self.client.get("/api/documents/?original_filename__iexact=docx.pdf")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 0)
+
+ response = self.client.get("/api/documents/?original_filename__istartswith=dOc")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 3)
+ self.assertCountEqual(
+ [results[0]["id"], results[1]["id"], results[2]["id"]],
+ [doc1.id, doc2.id, doc3.id],
+ )
+
+ def test_documents_title_content_filter(self):
+ doc1 = Document.objects.create(
+ title="title A",
+ content="content A",
+ checksum="A",
+ mime_type="application/pdf",
+ )
+ doc2 = Document.objects.create(
+ title="title B",
+ content="content A",
+ checksum="B",
+ mime_type="application/pdf",
+ )
+ doc3 = Document.objects.create(
+ title="title A",
+ content="content B",
+ checksum="C",
+ mime_type="application/pdf",
+ )
+ doc4 = Document.objects.create(
+ title="title B",
+ content="content B",
+ checksum="D",
+ mime_type="application/pdf",
+ )
+
+ response = self.client.get("/api/documents/?title_content=A")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 3)
+ self.assertCountEqual(
+ [results[0]["id"], results[1]["id"], results[2]["id"]],
+ [doc1.id, doc2.id, doc3.id],
+ )
+
+ response = self.client.get("/api/documents/?title_content=B")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 3)
+ self.assertCountEqual(
+ [results[0]["id"], results[1]["id"], results[2]["id"]],
+ [doc2.id, doc3.id, doc4.id],
+ )
+
+ response = self.client.get("/api/documents/?title_content=X")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 0)
+
+ def test_document_owner_filters(self):
+ """
+ GIVEN:
+ - Documents with owners, with and without granted permissions
+ WHEN:
+ - User filters by owner
+ THEN:
+ - Owner filters work correctly but still respect permissions
+ """
+ u1 = User.objects.create_user("user1")
+ u2 = User.objects.create_user("user2")
+ u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+ u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+
+ u1_doc1 = Document.objects.create(
+ title="none1",
+ checksum="A",
+ mime_type="application/pdf",
+ owner=u1,
+ )
+ Document.objects.create(
+ title="none2",
+ checksum="B",
+ mime_type="application/pdf",
+ owner=u2,
+ )
+ u0_doc1 = Document.objects.create(
+ title="none3",
+ checksum="C",
+ mime_type="application/pdf",
+ )
+ u1_doc2 = Document.objects.create(
+ title="none4",
+ checksum="D",
+ mime_type="application/pdf",
+ owner=u1,
+ )
+ u2_doc2 = Document.objects.create(
+ title="none5",
+ checksum="E",
+ mime_type="application/pdf",
+ owner=u2,
+ )
+
+ self.client.force_authenticate(user=u1)
+ assign_perm("view_document", u1, u2_doc2)
+
+ # Will not show any u1 docs or u2_doc1 which isn't shared
+ response = self.client.get(f"/api/documents/?owner__id__none={u1.id}")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+ self.assertCountEqual(
+ [results[0]["id"], results[1]["id"]],
+ [u0_doc1.id, u2_doc2.id],
+ )
+
+ # Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared
+ response = self.client.get(
+ f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertCountEqual([results[0]["id"]], [u2_doc2.id])
+
+ # Will not show any u1 docs, u2_doc2 which is shared but has owner
+ response = self.client.get(
+ f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertCountEqual([results[0]["id"]], [u0_doc1.id])
+
+ # Will not show any u1 docs or u2_doc1 which is not shared
+ response = self.client.get(f"/api/documents/?owner__id={u2.id}")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+ self.assertCountEqual([results[0]["id"]], [u2_doc2.id])
+
+ # Will not show u2_doc1 which is not shared
+ response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 3)
+ self.assertCountEqual(
+ [results[0]["id"], results[1]["id"], results[2]["id"]],
+ [u1_doc1.id, u1_doc2.id, u2_doc2.id],
+ )
+
+ def test_pagination_all(self):
+ """
+ GIVEN:
+ - A set of 50 documents
+ WHEN:
+ - API request for document filtering
+ THEN:
+ - Results are paginated (25 items) and response["all"] returns all ids (50 items)
+ """
+ t = Tag.objects.create(name="tag")
+ docs = []
+ for i in range(50):
+ d = Document.objects.create(checksum=i, content=f"test{i}")
+ d.tags.add(t)
+ docs.append(d)
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__in={t.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 25)
+ self.assertEqual(len(response.data["all"]), 50)
+ self.assertCountEqual(response.data["all"], [d.id for d in docs])
+
+ def test_statistics(self):
+ doc1 = Document.objects.create(
+ title="none1",
+ checksum="A",
+ mime_type="application/pdf",
+ content="abc",
+ )
+ Document.objects.create(
+ title="none2",
+ checksum="B",
+ mime_type="application/pdf",
+ content="123",
+ )
+ Document.objects.create(
+ title="none3",
+ checksum="C",
+ mime_type="text/plain",
+ content="hello",
+ )
+
+ tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
+ Tag.objects.create(name="t2")
+ Tag.objects.create(name="t3")
+ Correspondent.objects.create(name="c1")
+ Correspondent.objects.create(name="c2")
+ DocumentType.objects.create(name="dt1")
+ StoragePath.objects.create(name="sp1")
+ StoragePath.objects.create(name="sp2")
+
+ doc1.tags.add(tag_inbox)
+
+ response = self.client.get("/api/statistics/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["documents_total"], 3)
+ self.assertEqual(response.data["documents_inbox"], 1)
+ self.assertEqual(response.data["inbox_tag"], tag_inbox.pk)
+ self.assertEqual(
+ response.data["document_file_type_counts"][0]["mime_type_count"],
+ 2,
+ )
+ self.assertEqual(
+ response.data["document_file_type_counts"][1]["mime_type_count"],
+ 1,
+ )
+ self.assertEqual(response.data["character_count"], 11)
+ self.assertEqual(response.data["tag_count"], 3)
+ self.assertEqual(response.data["correspondent_count"], 2)
+ self.assertEqual(response.data["document_type_count"], 1)
+ self.assertEqual(response.data["storage_path_count"], 2)
+
+ def test_statistics_no_inbox_tag(self):
+ Document.objects.create(title="none1", checksum="A")
+
+ response = self.client.get("/api/statistics/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["documents_inbox"], None)
+ self.assertEqual(response.data["inbox_tag"], None)
+
+ def test_upload(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f},
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ input_doc, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(input_doc.original_file.name, "simple.pdf")
+ self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents)
+ self.assertIsNone(overrides.title)
+ self.assertIsNone(overrides.correspondent_id)
+ self.assertIsNone(overrides.document_type_id)
+ self.assertIsNone(overrides.tag_ids)
+
+ def test_upload_empty_metadata(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "title": "", "correspondent": "", "document_type": ""},
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ input_doc, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(input_doc.original_file.name, "simple.pdf")
+ self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents)
+ self.assertIsNone(overrides.title)
+ self.assertIsNone(overrides.correspondent_id)
+ self.assertIsNone(overrides.document_type_id)
+ self.assertIsNone(overrides.tag_ids)
+
+ def test_upload_invalid_form(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"documenst": f},
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.consume_file_mock.assert_not_called()
+
+ def test_upload_invalid_file(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.zip"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f},
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.consume_file_mock.assert_not_called()
+
+ def test_upload_with_title(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "title": "my custom title"},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ _, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(overrides.title, "my custom title")
+ self.assertIsNone(overrides.correspondent_id)
+ self.assertIsNone(overrides.document_type_id)
+ self.assertIsNone(overrides.tag_ids)
+
+ def test_upload_with_correspondent(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ c = Correspondent.objects.create(name="test-corres")
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "correspondent": c.id},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ _, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(overrides.correspondent_id, c.id)
+ self.assertIsNone(overrides.title)
+ self.assertIsNone(overrides.document_type_id)
+ self.assertIsNone(overrides.tag_ids)
+
+ def test_upload_with_invalid_correspondent(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "correspondent": 3456},
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ self.consume_file_mock.assert_not_called()
+
+ def test_upload_with_document_type(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ dt = DocumentType.objects.create(name="invoice")
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "document_type": dt.id},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ _, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(overrides.document_type_id, dt.id)
+ self.assertIsNone(overrides.correspondent_id)
+ self.assertIsNone(overrides.title)
+ self.assertIsNone(overrides.tag_ids)
+
+ def test_upload_with_invalid_document_type(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "document_type": 34578},
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ self.consume_file_mock.assert_not_called()
+
+ def test_upload_with_tags(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ t1 = Tag.objects.create(name="tag1")
+ t2 = Tag.objects.create(name="tag2")
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "tags": [t2.id, t1.id]},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ _, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertCountEqual(overrides.tag_ids, [t1.id, t2.id])
+ self.assertIsNone(overrides.document_type_id)
+ self.assertIsNone(overrides.correspondent_id)
+ self.assertIsNone(overrides.title)
+
+ def test_upload_with_invalid_tags(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ t1 = Tag.objects.create(name="tag1")
+ t2 = Tag.objects.create(name="tag2")
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "tags": [t2.id, t1.id, 734563]},
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ self.consume_file_mock.assert_not_called()
+
+ def test_upload_with_created(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ created = datetime.datetime(
+ 2022,
+ 5,
+ 12,
+ 0,
+ 0,
+ 0,
+ 0,
+ tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
+ )
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "created": created},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ _, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(overrides.created, created)
+
+ def test_upload_with_asn(self):
+ self.consume_file_mock.return_value = celery.result.AsyncResult(
+ id=str(uuid.uuid4()),
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ "rb",
+ ) as f:
+ response = self.client.post(
+ "/api/documents/post_document/",
+ {"document": f, "archive_serial_number": 500},
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.consume_file_mock.assert_called_once()
+
+ input_doc, overrides = self.get_last_consume_delay_call_args()
+
+ self.assertEqual(input_doc.original_file.name, "simple.pdf")
+ self.assertEqual(overrides.filename, "simple.pdf")
+ self.assertIsNone(overrides.correspondent_id)
+ self.assertIsNone(overrides.document_type_id)
+ self.assertIsNone(overrides.tag_ids)
+ self.assertEqual(500, overrides.asn)
+
+ def test_get_metadata(self):
+ doc = Document.objects.create(
+ title="test",
+ filename="file.pdf",
+ mime_type="image/png",
+ archive_checksum="A",
+ archive_filename="archive.pdf",
+ )
+
+ source_file = os.path.join(
+ os.path.dirname(__file__),
+ "samples",
+ "documents",
+ "thumbnails",
+ "0000001.webp",
+ )
+ archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
+
+ shutil.copy(source_file, doc.source_path)
+ shutil.copy(archive_file, doc.archive_path)
+
+ response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ meta = response.data
+
+ self.assertEqual(meta["original_mime_type"], "image/png")
+ self.assertTrue(meta["has_archive_version"])
+ self.assertEqual(len(meta["original_metadata"]), 0)
+ self.assertGreater(len(meta["archive_metadata"]), 0)
+ self.assertEqual(meta["media_filename"], "file.pdf")
+ self.assertEqual(meta["archive_media_filename"], "archive.pdf")
+ self.assertEqual(meta["original_size"], os.stat(source_file).st_size)
+ self.assertEqual(meta["archive_size"], os.stat(archive_file).st_size)
+
+ def test_get_metadata_invalid_doc(self):
+ response = self.client.get("/api/documents/34576/metadata/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_get_metadata_no_archive(self):
+ doc = Document.objects.create(
+ title="test",
+ filename="file.pdf",
+ mime_type="application/pdf",
+ )
+
+ shutil.copy(
+ os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
+ doc.source_path,
+ )
+
+ response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ meta = response.data
+
+ self.assertEqual(meta["original_mime_type"], "application/pdf")
+ self.assertFalse(meta["has_archive_version"])
+ self.assertGreater(len(meta["original_metadata"]), 0)
+ self.assertIsNone(meta["archive_metadata"])
+ self.assertIsNone(meta["archive_media_filename"])
+
+ def test_get_metadata_missing_files(self):
+ doc = Document.objects.create(
+ title="test",
+ filename="file.pdf",
+ mime_type="application/pdf",
+ archive_filename="file.pdf",
+ archive_checksum="B",
+ checksum="A",
+ )
+
+ response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ meta = response.data
+
+ self.assertTrue(meta["has_archive_version"])
+ self.assertIsNone(meta["original_metadata"])
+ self.assertIsNone(meta["original_size"])
+ self.assertIsNone(meta["archive_metadata"])
+ self.assertIsNone(meta["archive_size"])
+
+ def test_get_empty_suggestions(self):
+ doc = Document.objects.create(title="test", mime_type="application/pdf")
+
+ response = self.client.get(f"/api/documents/{doc.pk}/suggestions/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ response.data,
+ {
+ "correspondents": [],
+ "tags": [],
+ "document_types": [],
+ "storage_paths": [],
+ "dates": [],
+ },
+ )
+
+ def test_get_suggestions_invalid_doc(self):
+ response = self.client.get("/api/documents/34676/suggestions/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ @mock.patch("documents.views.match_storage_paths")
+ @mock.patch("documents.views.match_document_types")
+ @mock.patch("documents.views.match_tags")
+ @mock.patch("documents.views.match_correspondents")
+ @override_settings(NUMBER_OF_SUGGESTED_DATES=10)
+ def test_get_suggestions(
+ self,
+ match_correspondents,
+ match_tags,
+ match_document_types,
+ match_storage_paths,
+ ):
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is an invoice from 12.04.2022!",
+ )
+
+ match_correspondents.return_value = [Correspondent(id=88), Correspondent(id=2)]
+ match_tags.return_value = [Tag(id=56), Tag(id=123)]
+ match_document_types.return_value = [DocumentType(id=23)]
+ match_storage_paths.return_value = [StoragePath(id=99), StoragePath(id=77)]
+
+ response = self.client.get(f"/api/documents/{doc.pk}/suggestions/")
+ self.assertEqual(
+ response.data,
+ {
+ "correspondents": [88, 2],
+ "tags": [56, 123],
+ "document_types": [23],
+ "storage_paths": [99, 77],
+ "dates": ["2022-04-12"],
+ },
+ )
+
+ @mock.patch("documents.parsers.parse_date_generator")
+ @override_settings(NUMBER_OF_SUGGESTED_DATES=0)
+ def test_get_suggestions_dates_disabled(
+ self,
+ parse_date_generator,
+ ):
+ """
+ GIVEN:
+ - NUMBER_OF_SUGGESTED_DATES = 0 (disables feature)
+ WHEN:
+ - API reuqest for document suggestions
+ THEN:
+ - Dont check for suggested dates at all
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is an invoice from 12.04.2022!",
+ )
+
+ self.client.get(f"/api/documents/{doc.pk}/suggestions/")
+ self.assertFalse(parse_date_generator.called)
+
+ def test_saved_views(self):
+ u1 = User.objects.create_superuser("user1")
+ u2 = User.objects.create_superuser("user2")
+
+ v1 = SavedView.objects.create(
+ owner=u1,
+ name="test1",
+ sort_field="",
+ show_on_dashboard=False,
+ show_in_sidebar=False,
+ )
+ SavedView.objects.create(
+ owner=u2,
+ name="test2",
+ sort_field="",
+ show_on_dashboard=False,
+ show_in_sidebar=False,
+ )
+ SavedView.objects.create(
+ owner=u2,
+ name="test3",
+ sort_field="",
+ show_on_dashboard=False,
+ show_in_sidebar=False,
+ )
+
+ response = self.client.get("/api/saved_views/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 0)
+
+ self.assertEqual(
+ self.client.get(f"/api/saved_views/{v1.id}/").status_code,
+ status.HTTP_404_NOT_FOUND,
+ )
+
+ self.client.force_authenticate(user=u1)
+
+ response = self.client.get("/api/saved_views/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 1)
+
+ self.assertEqual(
+ self.client.get(f"/api/saved_views/{v1.id}/").status_code,
+ status.HTTP_200_OK,
+ )
+
+ self.client.force_authenticate(user=u2)
+
+ response = self.client.get("/api/saved_views/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 2)
+
+ self.assertEqual(
+ self.client.get(f"/api/saved_views/{v1.id}/").status_code,
+ status.HTTP_404_NOT_FOUND,
+ )
+
+ def test_create_update_patch(self):
+ User.objects.create_user("user1")
+
+ view = {
+ "name": "test",
+ "show_on_dashboard": True,
+ "show_in_sidebar": True,
+ "sort_field": "created2",
+ "filter_rules": [{"rule_type": 4, "value": "test"}],
+ }
+
+ response = self.client.post("/api/saved_views/", view, format="json")
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ v1 = SavedView.objects.get(name="test")
+ self.assertEqual(v1.sort_field, "created2")
+ self.assertEqual(v1.filter_rules.count(), 1)
+ self.assertEqual(v1.owner, self.user)
+
+ response = self.client.patch(
+ f"/api/saved_views/{v1.id}/",
+ {"show_in_sidebar": False},
+ format="json",
+ )
+
+ v1 = SavedView.objects.get(id=v1.id)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertFalse(v1.show_in_sidebar)
+ self.assertEqual(v1.filter_rules.count(), 1)
+
+ view["filter_rules"] = [{"rule_type": 12, "value": "secret"}]
+
+ response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ v1 = SavedView.objects.get(id=v1.id)
+ self.assertEqual(v1.filter_rules.count(), 1)
+ self.assertEqual(v1.filter_rules.first().value, "secret")
+
+ view["filter_rules"] = []
+
+ response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ v1 = SavedView.objects.get(id=v1.id)
+ self.assertEqual(v1.filter_rules.count(), 0)
+
+ def test_get_logs(self):
+ log_data = "test\ntest2\n"
+ with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f:
+ f.write(log_data)
+ with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
+ f.write(log_data)
+ response = self.client.get("/api/logs/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertCountEqual(response.data, ["mail", "paperless"])
+
+ def test_get_logs_only_when_exist(self):
+ log_data = "test\ntest2\n"
+ with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
+ f.write(log_data)
+ response = self.client.get("/api/logs/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertCountEqual(response.data, ["paperless"])
+
+ def test_get_invalid_log(self):
+ response = self.client.get("/api/logs/bogus_log/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ @override_settings(LOGGING_DIR="bogus_dir")
+ def test_get_nonexistent_log(self):
+ response = self.client.get("/api/logs/paperless/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_get_log(self):
+ log_data = "test\ntest2\n"
+ with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
+ f.write(log_data)
+ response = self.client.get("/api/logs/paperless/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertListEqual(response.data, ["test", "test2"])
+
+ def test_invalid_regex_other_algorithm(self):
+ for endpoint in ["correspondents", "tags", "document_types"]:
+ response = self.client.post(
+ f"/api/{endpoint}/",
+ {
+ "name": "test",
+ "matching_algorithm": MatchingModel.MATCH_ANY,
+ "match": "[",
+ },
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint)
+
+ def test_invalid_regex(self):
+ for endpoint in ["correspondents", "tags", "document_types"]:
+ response = self.client.post(
+ f"/api/{endpoint}/",
+ {
+ "name": "test",
+ "matching_algorithm": MatchingModel.MATCH_REGEX,
+ "match": "[",
+ },
+ format="json",
+ )
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_400_BAD_REQUEST,
+ endpoint,
+ )
+
+ def test_valid_regex(self):
+ for endpoint in ["correspondents", "tags", "document_types"]:
+ response = self.client.post(
+ f"/api/{endpoint}/",
+ {
+ "name": "test",
+ "matching_algorithm": MatchingModel.MATCH_REGEX,
+ "match": "[0-9]",
+ },
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint)
+
+ def test_regex_no_algorithm(self):
+ for endpoint in ["correspondents", "tags", "document_types"]:
+ response = self.client.post(
+ f"/api/{endpoint}/",
+ {"name": "test", "match": "[0-9]"},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint)
+
+ def test_tag_color_default(self):
+ response = self.client.post("/api/tags/", {"name": "tag"}, format="json")
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#a6cee3")
+ self.assertEqual(
+ self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[
+ "colour"
+ ],
+ 1,
+ )
+
+ def test_tag_color(self):
+ response = self.client.post(
+ "/api/tags/",
+ {"name": "tag", "colour": 3},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#b2df8a")
+ self.assertEqual(
+ self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[
+ "colour"
+ ],
+ 3,
+ )
+
+ def test_tag_color_invalid(self):
+ response = self.client.post(
+ "/api/tags/",
+ {"name": "tag", "colour": 34},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_tag_color_custom(self):
+ tag = Tag.objects.create(name="test", color="#abcdef")
+ self.assertEqual(
+ self.client.get(f"/api/tags/{tag.id}/", format="json").data["colour"],
+ 1,
+ )
+
+ def test_get_existing_notes(self):
+ """
+ GIVEN:
+ - A document with a single note
+ WHEN:
+ - API reuqest for document notes is made
+ THEN:
+ - The associated note is returned
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document which will have notes!",
+ )
+ note = Note.objects.create(
+ note="This is a note.",
+ document=doc,
+ user=self.user,
+ )
+
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/notes/",
+ 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]
+ del resp_data["created"]
+
+ self.assertDictEqual(
+ resp_data,
+ {
+ "id": note.id,
+ "note": note.note,
+ "user": {
+ "id": note.user.id,
+ "username": note.user.username,
+ "first_name": note.user.first_name,
+ "last_name": note.user.last_name,
+ },
+ },
+ )
+
+ def test_create_note(self):
+ """
+ GIVEN:
+ - Existing document
+ WHEN:
+ - API request is made to add a note
+ THEN:
+ - note is created and associated with document, modified time is updated
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document which will have notes 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}/notes/",
+ data={"note": "this is a posted note"},
+ )
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/notes/",
+ 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["note"], "this is a posted note")
+
+ doc = Document.objects.get(pk=doc.pk)
+ # modified was updated to today
+ self.assertEqual(doc.modified.day, timezone.now().day)
+
+ def test_notes_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 note or delete
+ THEN:
+ - Notes are neither created nor deleted
+ """
+ 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 notes added",
+ )
+ doc.owner = user2
+ doc.save()
+
+ self.client.force_authenticate(user1)
+
+ resp = self.client.get(
+ f"/api/documents/{doc.pk}/notes/",
+ format="json",
+ )
+ self.assertEqual(resp.content, b"Insufficient permissions to view notes")
+ self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+ assign_perm("view_document", user1, doc)
+
+ resp = self.client.post(
+ f"/api/documents/{doc.pk}/notes/",
+ data={"note": "this is a posted note"},
+ )
+ self.assertEqual(resp.content, b"Insufficient permissions to create notes")
+ self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+ note = Note.objects.create(
+ note="This is a note.",
+ document=doc,
+ user=user2,
+ )
+
+ response = self.client.delete(
+ f"/api/documents/{doc.pk}/notes/?id={note.pk}",
+ format="json",
+ )
+
+ self.assertEqual(response.content, b"Insufficient permissions to delete notes")
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_delete_note(self):
+ """
+ GIVEN:
+ - Existing document, existing note
+ WHEN:
+ - API request is made to delete a note
+ THEN:
+ - note is deleted, document modified is updated
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document which will have notes!",
+ 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)
+ note = Note.objects.create(
+ note="This is a note.",
+ document=doc,
+ user=self.user,
+ )
+
+ response = self.client.delete(
+ f"/api/documents/{doc.pk}/notes/?id={note.pk}",
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.assertEqual(len(Note.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_notes_no_doc(self):
+ """
+ GIVEN:
+ - A request to get notes from a non-existent document
+ WHEN:
+ - API request for document notes is made
+ THEN:
+ - HTTP status.HTTP_404_NOT_FOUND is returned
+ """
+ response = self.client.get(
+ "/api/documents/500/notes/",
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_tag_unique_name_and_owner(self):
+ """
+ GIVEN:
+ - Multiple users
+ - Tags owned by particular users
+ WHEN:
+ - API request for creating items which are unique by name and owner
+ THEN:
+ - Unique items are created
+ - Non-unique items are not allowed
+ """
+ user1 = User.objects.create_user(username="test1")
+ user1.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
+ user1.save()
+
+ user2 = User.objects.create_user(username="test2")
+ user2.user_permissions.add(*Permission.objects.filter(codename="add_tag"))
+ user2.save()
+
+ # User 1 creates tag 1 owned by user 1 by default
+ # No issue
+ self.client.force_authenticate(user1)
+ response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json")
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ # User 2 creates tag 1 owned by user 2 by default
+ # No issue
+ self.client.force_authenticate(user2)
+ response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json")
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ # User 2 creates tag 2 owned by user 1
+ # No issue
+ self.client.force_authenticate(user2)
+ response = self.client.post(
+ "/api/tags/",
+ {"name": "tag 2", "owner": user1.pk},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ # User 1 creates tag 2 owned by user 1 by default
+ # Not allowed, would create tag2/user1 which already exists
+ self.client.force_authenticate(user1)
+ response = self.client.post(
+ "/api/tags/",
+ {"name": "tag 2"},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # User 1 creates tag 2 owned by user 1
+ # Not allowed, would create tag2/user1 which already exists
+ response = self.client.post(
+ "/api/tags/",
+ {"name": "tag 2", "owner": user1.pk},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_tag_unique_name_and_owner_enforced_on_update(self):
+ """
+ GIVEN:
+ - Multiple users
+ - Tags owned by particular users
+ WHEN:
+ - API request for to update tag in such as way as makes it non-unqiue
+ THEN:
+ - Unique items are created
+ - Non-unique items are not allowed on update
+ """
+ user1 = User.objects.create_user(username="test1")
+ user1.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
+ user1.save()
+
+ user2 = User.objects.create_user(username="test2")
+ user2.user_permissions.add(*Permission.objects.filter(codename="change_tag"))
+ user2.save()
+
+ # Create name tag 1 owned by user 1
+ # Create name tag 1 owned by user 2
+ Tag.objects.create(name="tag 1", owner=user1)
+ tag2 = Tag.objects.create(name="tag 1", owner=user2)
+
+ # User 2 attempts to change the owner of tag to user 1
+ # Not allowed, would change to tag1/user1 which already exists
+ self.client.force_authenticate(user2)
+ response = self.client.patch(
+ f"/api/tags/{tag2.id}/",
+ {"owner": user1.pk},
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_share_links(self):
+ """
+ GIVEN:
+ - Existing document
+ WHEN:
+ - API request is made to generate a share_link
+ - API request is made to view share_links on incorrect doc pk
+ - Invalid method request is made to view share_links doc
+ THEN:
+ - Link is created with a slug and associated with document
+ - 404
+ - Error
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document which will have notes added",
+ )
+ # never expires
+ resp = self.client.post(
+ "/api/share_links/",
+ data={
+ "document": doc.pk,
+ },
+ )
+ self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
+
+ resp = self.client.post(
+ "/api/share_links/",
+ data={
+ "expiration": (timezone.now() + timedelta(days=7)).isoformat(),
+ "document": doc.pk,
+ "file_version": "original",
+ },
+ )
+ self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
+
+ response = self.client.get(
+ f"/api/documents/{doc.pk}/share_links/",
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ resp_data = response.json()
+
+ self.assertEqual(len(resp_data), 2)
+
+ self.assertGreater(len(resp_data[1]["slug"]), 0)
+ self.assertIsNone(resp_data[1]["expiration"])
+ self.assertEqual(
+ (parser.isoparse(resp_data[0]["expiration"]) - timezone.now()).days,
+ 6,
+ )
+
+ sl1 = ShareLink.objects.get(slug=resp_data[1]["slug"])
+ self.assertEqual(str(sl1), f"Share Link for {doc.title}")
+
+ response = self.client.post(
+ f"/api/documents/{doc.pk}/share_links/",
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
+
+ response = self.client.get(
+ "/api/documents/99/share_links/",
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_share_links_permissions_aware(self):
+ """
+ GIVEN:
+ - Existing document owned by user2 but with granted view perms for user1
+ WHEN:
+ - API request is made by user1 to view share links
+ THEN:
+ - Links only shown if user has permissions
+ """
+ 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 share links added",
+ )
+ doc.owner = user2
+ doc.save()
+
+ self.client.force_authenticate(user1)
+
+ resp = self.client.get(
+ f"/api/documents/{doc.pk}/share_links/",
+ format="json",
+ )
+ self.assertEqual(resp.content, b"Insufficient permissions to add share link")
+ self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+ assign_perm("change_document", user1, doc)
+
+ resp = self.client.get(
+ f"/api/documents/{doc.pk}/share_links/",
+ format="json",
+ )
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+
+ def test_next_asn(self):
+ """
+ GIVEN:
+ - Existing documents with ASNs, highest owned by user2
+ WHEN:
+ - API request is made by user1 to get next ASN
+ THEN:
+ - ASN +1 from user2's doc is returned for user1
+ """
+ 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()
+
+ doc1 = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document 1",
+ checksum="1",
+ archive_serial_number=998,
+ )
+ doc1.owner = user1
+ doc1.save()
+
+ doc2 = Document.objects.create(
+ title="test2",
+ mime_type="application/pdf",
+ content="this is a document 2 with higher ASN",
+ checksum="2",
+ archive_serial_number=999,
+ )
+ doc2.owner = user2
+ doc2.save()
+
+ self.client.force_authenticate(user1)
+
+ resp = self.client.get(
+ "/api/documents/next_asn/",
+ )
+ self.assertEqual(resp.status_code, status.HTTP_200_OK)
+ self.assertEqual(resp.content, b"1000")
+
+
+class TestDocumentApiV2(DirectoriesMixin, APITestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.user = User.objects.create_superuser(username="temp_admin")
+
+ self.client.force_authenticate(user=self.user)
+ self.client.defaults["HTTP_ACCEPT"] = "application/json; version=2"
+
+ def test_tag_validate_color(self):
+ self.assertEqual(
+ self.client.post(
+ "/api/tags/",
+ {"name": "test", "color": "#12fFaA"},
+ format="json",
+ ).status_code,
+ status.HTTP_201_CREATED,
+ )
+
+ self.assertEqual(
+ self.client.post(
+ "/api/tags/",
+ {"name": "test1", "color": "abcdef"},
+ format="json",
+ ).status_code,
+ status.HTTP_400_BAD_REQUEST,
+ )
+ self.assertEqual(
+ self.client.post(
+ "/api/tags/",
+ {"name": "test2", "color": "#abcdfg"},
+ format="json",
+ ).status_code,
+ status.HTTP_400_BAD_REQUEST,
+ )
+ self.assertEqual(
+ self.client.post(
+ "/api/tags/",
+ {"name": "test3", "color": "#asd"},
+ format="json",
+ ).status_code,
+ status.HTTP_400_BAD_REQUEST,
+ )
+ self.assertEqual(
+ self.client.post(
+ "/api/tags/",
+ {"name": "test4", "color": "#12121212"},
+ format="json",
+ ).status_code,
+ status.HTTP_400_BAD_REQUEST,
+ )
+
+ def test_tag_text_color(self):
+ t = Tag.objects.create(name="tag1", color="#000000")
+ self.assertEqual(
+ self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
+ "#ffffff",
+ )
+
+ t.color = "#ffffff"
+ t.save()
+ self.assertEqual(
+ self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
+ "#000000",
+ )
+
+ t.color = "asdf"
+ t.save()
+ self.assertEqual(
+ self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
+ "#000000",
+ )
+
+ t.color = "123"
+ t.save()
+ self.assertEqual(
+ self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"],
+ "#000000",
+ )
--- /dev/null
+import json
+from unittest import mock
+
+from django.contrib.auth.models import User
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.models import Correspondent
+from documents.models import Document
+from documents.models import DocumentType
+from documents.models import StoragePath
+from documents.models import Tag
+from documents.tests.utils import DirectoriesMixin
+
+
+class TestApiObjects(DirectoriesMixin, APITestCase):
+ def setUp(self) -> None:
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+
+ self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True)
+ self.tag2 = Tag.objects.create(name="t2")
+ self.tag3 = Tag.objects.create(name="t3")
+ self.c1 = Correspondent.objects.create(name="c1")
+ self.c2 = Correspondent.objects.create(name="c2")
+ self.c3 = Correspondent.objects.create(name="c3")
+ self.dt1 = DocumentType.objects.create(name="dt1")
+ self.dt2 = DocumentType.objects.create(name="dt2")
+ self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{title}")
+ self.sp2 = StoragePath.objects.create(name="sp2", path="Something2/{title}")
+
+ def test_object_filters(self):
+ response = self.client.get(
+ f"/api/tags/?id={self.tag2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+
+ response = self.client.get(
+ f"/api/tags/?id__in={self.tag1.id},{self.tag3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+
+ response = self.client.get(
+ f"/api/correspondents/?id={self.c2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+
+ response = self.client.get(
+ f"/api/correspondents/?id__in={self.c1.id},{self.c3.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+
+ response = self.client.get(
+ f"/api/document_types/?id={self.dt1.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+
+ response = self.client.get(
+ f"/api/document_types/?id__in={self.dt1.id},{self.dt2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+
+ response = self.client.get(
+ f"/api/storage_paths/?id={self.sp1.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 1)
+
+ response = self.client.get(
+ f"/api/storage_paths/?id__in={self.sp1.id},{self.sp2.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 2)
+
+
+class TestApiStoragePaths(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/storage_paths/"
+
+ def setUp(self) -> None:
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+
+ self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
+
+ def test_api_get_storage_path(self):
+ """
+ GIVEN:
+ - API request to get all storage paths
+ WHEN:
+ - API is called
+ THEN:
+ - Existing storage paths are returned
+ """
+ response = self.client.get(self.ENDPOINT, format="json")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 1)
+
+ resp_storage_path = response.data["results"][0]
+ self.assertEqual(resp_storage_path["id"], self.sp1.id)
+ self.assertEqual(resp_storage_path["path"], self.sp1.path)
+
+ def test_api_create_storage_path(self):
+ """
+ GIVEN:
+ - API request to create a storage paths
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ - New storage path is created
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "A storage path",
+ "path": "Somewhere/{asn}",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(StoragePath.objects.count(), 2)
+
+ def test_api_create_invalid_storage_path(self):
+ """
+ GIVEN:
+ - API request to create a storage paths
+ - Storage path format is incorrect
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP 400 response
+ - No storage path is created
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Another storage path",
+ "path": "Somewhere/{correspdent}",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(StoragePath.objects.count(), 1)
+
+ def test_api_storage_path_placeholders(self):
+ """
+ GIVEN:
+ - API request to create a storage path with placeholders
+ - Storage path is valid
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ - New storage path is created
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Storage path with placeholders",
+ "path": "{title}/{correspondent}/{document_type}/{created}/{created_year}"
+ "/{created_year_short}/{created_month}/{created_month_name}"
+ "/{created_month_name_short}/{created_day}/{added}/{added_year}"
+ "/{added_year_short}/{added_month}/{added_month_name}"
+ "/{added_month_name_short}/{added_day}/{asn}/{tags}"
+ "/{tag_list}/{owner_username}/{original_name}/{doc_pk}/",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(StoragePath.objects.count(), 2)
+
+ @mock.patch("documents.bulk_edit.bulk_update_documents.delay")
+ def test_api_update_storage_path(self, bulk_update_mock):
+ """
+ GIVEN:
+ - API request to get all storage paths
+ WHEN:
+ - API is called
+ THEN:
+ - Existing storage paths are returned
+ """
+ document = Document.objects.create(
+ mime_type="application/pdf",
+ storage_path=self.sp1,
+ )
+ response = self.client.patch(
+ f"{self.ENDPOINT}{self.sp1.pk}/",
+ data={
+ "path": "somewhere/{created} - {title}",
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ bulk_update_mock.assert_called_once()
+
+ args, _ = bulk_update_mock.call_args
+
+ self.assertCountEqual([document.pk], args[0])
--- /dev/null
+import json
+
+from django.contrib.auth.models import Group
+from django.contrib.auth.models import Permission
+from django.contrib.auth.models import User
+from guardian.shortcuts import assign_perm
+from guardian.shortcuts import get_perms
+from guardian.shortcuts import get_users_with_perms
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.models import Correspondent
+from documents.models import Document
+from documents.models import DocumentType
+from documents.models import MatchingModel
+from documents.models import StoragePath
+from documents.models import Tag
+from documents.tests.utils import DirectoriesMixin
+
+
+class TestApiAuth(DirectoriesMixin, APITestCase):
+ def test_auth_required(self):
+ d = Document.objects.create(title="Test")
+
+ self.assertEqual(
+ self.client.get("/api/documents/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+
+ self.assertEqual(
+ self.client.get(f"/api/documents/{d.id}/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get(f"/api/documents/{d.id}/download/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get(f"/api/documents/{d.id}/preview/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get(f"/api/documents/{d.id}/thumb/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+
+ self.assertEqual(
+ self.client.get("/api/tags/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get("/api/correspondents/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get("/api/document_types/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+
+ self.assertEqual(
+ self.client.get("/api/logs/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get("/api/saved_views/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+
+ self.assertEqual(
+ self.client.get("/api/search/autocomplete/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get("/api/documents/bulk_edit/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get("/api/documents/bulk_download/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+ self.assertEqual(
+ self.client.get("/api/documents/selection_data/").status_code,
+ status.HTTP_401_UNAUTHORIZED,
+ )
+
+ def test_api_version_no_auth(self):
+ response = self.client.get("/api/")
+ self.assertNotIn("X-Api-Version", response)
+ self.assertNotIn("X-Version", response)
+
+ def test_api_version_with_auth(self):
+ user = User.objects.create_superuser(username="test")
+ self.client.force_authenticate(user)
+ response = self.client.get("/api/")
+ self.assertIn("X-Api-Version", response)
+ self.assertIn("X-Version", response)
+
+ def test_api_insufficient_permissions(self):
+ user = User.objects.create_user(username="test")
+ self.client.force_authenticate(user)
+
+ Document.objects.create(title="Test")
+
+ self.assertEqual(
+ self.client.get("/api/documents/").status_code,
+ status.HTTP_403_FORBIDDEN,
+ )
+
+ self.assertEqual(
+ self.client.get("/api/tags/").status_code,
+ status.HTTP_403_FORBIDDEN,
+ )
+ self.assertEqual(
+ self.client.get("/api/correspondents/").status_code,
+ status.HTTP_403_FORBIDDEN,
+ )
+ self.assertEqual(
+ self.client.get("/api/document_types/").status_code,
+ status.HTTP_403_FORBIDDEN,
+ )
+
+ self.assertEqual(
+ self.client.get("/api/logs/").status_code,
+ status.HTTP_403_FORBIDDEN,
+ )
+ self.assertEqual(
+ self.client.get("/api/saved_views/").status_code,
+ status.HTTP_403_FORBIDDEN,
+ )
+
+ def test_api_sufficient_permissions(self):
+ user = User.objects.create_user(username="test")
+ user.user_permissions.add(*Permission.objects.all())
+ self.client.force_authenticate(user)
+
+ Document.objects.create(title="Test")
+
+ self.assertEqual(
+ self.client.get("/api/documents/").status_code,
+ status.HTTP_200_OK,
+ )
+
+ self.assertEqual(self.client.get("/api/tags/").status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ self.client.get("/api/correspondents/").status_code,
+ status.HTTP_200_OK,
+ )
+ self.assertEqual(
+ self.client.get("/api/document_types/").status_code,
+ status.HTTP_200_OK,
+ )
+
+ self.assertEqual(self.client.get("/api/logs/").status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ self.client.get("/api/saved_views/").status_code,
+ status.HTTP_200_OK,
+ )
+
+ def test_api_get_object_permissions(self):
+ user1 = User.objects.create_user(username="test1")
+ user2 = User.objects.create_user(username="test2")
+ user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+ self.client.force_authenticate(user1)
+
+ self.assertEqual(
+ self.client.get("/api/documents/").status_code,
+ status.HTTP_200_OK,
+ )
+
+ d = Document.objects.create(title="Test", content="the content 1", checksum="1")
+
+ # no owner
+ self.assertEqual(
+ self.client.get(f"/api/documents/{d.id}/").status_code,
+ status.HTTP_200_OK,
+ )
+
+ d2 = Document.objects.create(
+ title="Test 2",
+ content="the content 2",
+ checksum="2",
+ owner=user2,
+ )
+
+ self.assertEqual(
+ self.client.get(f"/api/documents/{d2.id}/").status_code,
+ status.HTTP_404_NOT_FOUND,
+ )
+
+ def test_api_default_owner(self):
+ """
+ GIVEN:
+ - API request to create an object (Tag)
+ WHEN:
+ - owner is not set at all
+ THEN:
+ - Object created with current user as owner
+ """
+ user1 = User.objects.create_superuser(username="user1")
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.post(
+ "/api/tags/",
+ json.dumps(
+ {
+ "name": "test1",
+ "matching_algorithm": MatchingModel.MATCH_AUTO,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ tag1 = Tag.objects.filter(name="test1").first()
+ self.assertEqual(tag1.owner, user1)
+
+ def test_api_set_no_owner(self):
+ """
+ GIVEN:
+ - API request to create an object (Tag)
+ WHEN:
+ - owner is passed as None
+ THEN:
+ - Object created with no owner
+ """
+ user1 = User.objects.create_superuser(username="user1")
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.post(
+ "/api/tags/",
+ json.dumps(
+ {
+ "name": "test1",
+ "matching_algorithm": MatchingModel.MATCH_AUTO,
+ "owner": None,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ tag1 = Tag.objects.filter(name="test1").first()
+ self.assertEqual(tag1.owner, None)
+
+ def test_api_set_owner_w_permissions(self):
+ """
+ GIVEN:
+ - API request to create an object (Tag) that supplies set_permissions object
+ WHEN:
+ - owner is passed as user id
+ - view > users is set & view > groups is set
+ THEN:
+ - Object permissions are set appropriately
+ """
+ user1 = User.objects.create_superuser(username="user1")
+ user2 = User.objects.create(username="user2")
+ group1 = Group.objects.create(name="group1")
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.post(
+ "/api/tags/",
+ json.dumps(
+ {
+ "name": "test1",
+ "matching_algorithm": MatchingModel.MATCH_AUTO,
+ "owner": user1.id,
+ "set_permissions": {
+ "view": {
+ "users": [user2.id],
+ "groups": [group1.id],
+ },
+ "change": {
+ "users": None,
+ "groups": None,
+ },
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ tag1 = Tag.objects.filter(name="test1").first()
+
+ from guardian.core import ObjectPermissionChecker
+
+ checker = ObjectPermissionChecker(user2)
+ self.assertEqual(checker.has_perm("view_tag", tag1), True)
+ self.assertIn("view_tag", get_perms(group1, tag1))
+
+ def test_api_set_other_owner_w_permissions(self):
+ """
+ GIVEN:
+ - API request to create an object (Tag)
+ WHEN:
+ - a different owner than is logged in is set
+ - view > groups is set
+ THEN:
+ - Object permissions are set appropriately
+ """
+ user1 = User.objects.create_superuser(username="user1")
+ user2 = User.objects.create(username="user2")
+ group1 = Group.objects.create(name="group1")
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.post(
+ "/api/tags/",
+ json.dumps(
+ {
+ "name": "test1",
+ "matching_algorithm": MatchingModel.MATCH_AUTO,
+ "owner": user2.id,
+ "set_permissions": {
+ "view": {
+ "users": None,
+ "groups": [group1.id],
+ },
+ "change": {
+ "users": None,
+ "groups": None,
+ },
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ tag1 = Tag.objects.filter(name="test1").first()
+
+ self.assertEqual(tag1.owner, user2)
+ self.assertIn("view_tag", get_perms(group1, tag1))
+
+ def test_api_set_doc_permissions(self):
+ """
+ GIVEN:
+ - API request to update doc permissions and owner
+ WHEN:
+ - owner is set
+ - view > users is set & view > groups is set
+ THEN:
+ - Object permissions are set appropriately
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="this is a document",
+ )
+ user1 = User.objects.create_superuser(username="user1")
+ user2 = User.objects.create(username="user2")
+ group1 = Group.objects.create(name="group1")
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.patch(
+ f"/api/documents/{doc.id}/",
+ json.dumps(
+ {
+ "owner": user1.id,
+ "set_permissions": {
+ "view": {
+ "users": [user2.id],
+ "groups": [group1.id],
+ },
+ "change": {
+ "users": None,
+ "groups": None,
+ },
+ },
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ doc = Document.objects.get(pk=doc.id)
+
+ self.assertEqual(doc.owner, user1)
+ from guardian.core import ObjectPermissionChecker
+
+ checker = ObjectPermissionChecker(user2)
+ self.assertTrue(checker.has_perm("view_document", doc))
+ self.assertIn("view_document", get_perms(group1, doc))
+
+ def test_dynamic_permissions_fields(self):
+ user1 = User.objects.create_user(username="user1")
+ user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+ user2 = User.objects.create_user(username="user2")
+
+ Document.objects.create(title="Test", content="content 1", checksum="1")
+ doc2 = Document.objects.create(
+ title="Test2",
+ content="content 2",
+ checksum="2",
+ owner=user2,
+ )
+ doc3 = Document.objects.create(
+ title="Test3",
+ content="content 3",
+ checksum="3",
+ owner=user2,
+ )
+
+ assign_perm("view_document", user1, doc2)
+ assign_perm("view_document", user1, doc3)
+ assign_perm("change_document", user1, doc3)
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.get(
+ "/api/documents/",
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ resp_data = response.json()
+
+ self.assertNotIn("permissions", resp_data["results"][0])
+ self.assertIn("user_can_change", resp_data["results"][0])
+ self.assertEqual(resp_data["results"][0]["user_can_change"], True) # doc1
+ self.assertEqual(resp_data["results"][1]["user_can_change"], False) # doc2
+ self.assertEqual(resp_data["results"][2]["user_can_change"], True) # doc3
+
+ response = self.client.get(
+ "/api/documents/?full_perms=true",
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ resp_data = response.json()
+
+ self.assertIn("permissions", resp_data["results"][0])
+ self.assertNotIn("user_can_change", resp_data["results"][0])
+
+
+class TestApiUser(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/users/"
+
+ def setUp(self):
+ super().setUp()
+
+ self.user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=self.user)
+
+ def test_get_users(self):
+ """
+ GIVEN:
+ - Configured users
+ WHEN:
+ - API call is made to get users
+ THEN:
+ - Configured users are provided
+ """
+
+ user1 = User.objects.create(
+ username="testuser",
+ password="test",
+ first_name="Test",
+ last_name="User",
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 2)
+ returned_user2 = response.data["results"][1]
+
+ self.assertEqual(returned_user2["username"], user1.username)
+ self.assertEqual(returned_user2["password"], "**********")
+ self.assertEqual(returned_user2["first_name"], user1.first_name)
+ self.assertEqual(returned_user2["last_name"], user1.last_name)
+
+ def test_create_user(self):
+ """
+ WHEN:
+ - API request is made to add a user account
+ THEN:
+ - A new user account is created
+ """
+
+ user1 = {
+ "username": "testuser",
+ "password": "test",
+ "first_name": "Test",
+ "last_name": "User",
+ }
+
+ response = self.client.post(
+ self.ENDPOINT,
+ data=user1,
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ returned_user1 = User.objects.get(username="testuser")
+
+ self.assertEqual(returned_user1.username, user1["username"])
+ self.assertEqual(returned_user1.first_name, user1["first_name"])
+ self.assertEqual(returned_user1.last_name, user1["last_name"])
+
+ def test_delete_user(self):
+ """
+ GIVEN:
+ - Existing user account
+ WHEN:
+ - API request is made to delete a user account
+ THEN:
+ - Account is deleted
+ """
+
+ user1 = User.objects.create(
+ username="testuser",
+ password="test",
+ first_name="Test",
+ last_name="User",
+ )
+
+ nUsers = User.objects.count()
+
+ response = self.client.delete(
+ f"{self.ENDPOINT}{user1.pk}/",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ self.assertEqual(User.objects.count(), nUsers - 1)
+
+ def test_update_user(self):
+ """
+ GIVEN:
+ - Existing user accounts
+ WHEN:
+ - API request is made to update user account
+ THEN:
+ - The user account is updated, password only updated if not '****'
+ """
+
+ user1 = User.objects.create(
+ username="testuser",
+ password="test",
+ first_name="Test",
+ last_name="User",
+ )
+
+ initial_password = user1.password
+
+ response = self.client.patch(
+ f"{self.ENDPOINT}{user1.pk}/",
+ data={
+ "first_name": "Updated Name 1",
+ "password": "******",
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ returned_user1 = User.objects.get(pk=user1.pk)
+ self.assertEqual(returned_user1.first_name, "Updated Name 1")
+ self.assertEqual(returned_user1.password, initial_password)
+
+ response = self.client.patch(
+ f"{self.ENDPOINT}{user1.pk}/",
+ data={
+ "first_name": "Updated Name 2",
+ "password": "123xyz",
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ returned_user2 = User.objects.get(pk=user1.pk)
+ self.assertEqual(returned_user2.first_name, "Updated Name 2")
+ self.assertNotEqual(returned_user2.password, initial_password)
+
+
+class TestApiGroup(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/groups/"
+
+ def setUp(self):
+ super().setUp()
+
+ self.user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=self.user)
+
+ def test_get_groups(self):
+ """
+ GIVEN:
+ - Configured groups
+ WHEN:
+ - API call is made to get groups
+ THEN:
+ - Configured groups are provided
+ """
+
+ group1 = Group.objects.create(
+ name="Test Group",
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["count"], 1)
+ returned_group1 = response.data["results"][0]
+
+ self.assertEqual(returned_group1["name"], group1.name)
+
+ def test_create_group(self):
+ """
+ WHEN:
+ - API request is made to add a group
+ THEN:
+ - A new group is created
+ """
+
+ group1 = {
+ "name": "Test Group",
+ }
+
+ response = self.client.post(
+ self.ENDPOINT,
+ data=group1,
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ returned_group1 = Group.objects.get(name="Test Group")
+
+ self.assertEqual(returned_group1.name, group1["name"])
+
+ def test_delete_group(self):
+ """
+ GIVEN:
+ - Existing group
+ WHEN:
+ - API request is made to delete a group
+ THEN:
+ - Group is deleted
+ """
+
+ group1 = Group.objects.create(
+ name="Test Group",
+ )
+
+ response = self.client.delete(
+ f"{self.ENDPOINT}{group1.pk}/",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ self.assertEqual(len(Group.objects.all()), 0)
+
+ def test_update_group(self):
+ """
+ GIVEN:
+ - Existing groups
+ WHEN:
+ - API request is made to update group
+ THEN:
+ - The group is updated
+ """
+
+ group1 = Group.objects.create(
+ name="Test Group",
+ )
+
+ response = self.client.patch(
+ f"{self.ENDPOINT}{group1.pk}/",
+ data={
+ "name": "Updated Name 1",
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ returned_group1 = Group.objects.get(pk=group1.pk)
+ self.assertEqual(returned_group1.name, "Updated Name 1")
+
+
+class TestBulkEditObjectPermissions(APITestCase):
+ def setUp(self):
+ super().setUp()
+
+ user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=user)
+
+ self.t1 = Tag.objects.create(name="t1")
+ self.t2 = Tag.objects.create(name="t2")
+ self.c1 = Correspondent.objects.create(name="c1")
+ self.dt1 = DocumentType.objects.create(name="dt1")
+ self.sp1 = StoragePath.objects.create(name="sp1")
+ self.user1 = User.objects.create(username="user1")
+ self.user2 = User.objects.create(username="user2")
+ self.user3 = User.objects.create(username="user3")
+
+ def test_bulk_object_set_permissions(self):
+ """
+ GIVEN:
+ - Existing objects
+ WHEN:
+ - bulk_edit_object_perms API endpoint is called
+ THEN:
+ - Permissions and / or owner are changed
+ """
+ permissions = {
+ "view": {
+ "users": [self.user1.id, self.user2.id],
+ "groups": [],
+ },
+ "change": {
+ "users": [self.user1.id],
+ "groups": [],
+ },
+ }
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id],
+ "object_type": "tags",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.t1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.c1.id],
+ "object_type": "correspondents",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.c1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.dt1.id],
+ "object_type": "document_types",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.dt1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.sp1.id],
+ "object_type": "storage_paths",
+ "permissions": permissions,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn(self.user1, get_users_with_perms(self.sp1))
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id],
+ "object_type": "tags",
+ "owner": self.user3.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3)
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.sp1.id],
+ "object_type": "storage_paths",
+ "owner": self.user3.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3)
+
+ def test_bulk_edit_object_permissions_insufficient_perms(self):
+ """
+ GIVEN:
+ - Objects owned by user other than logged in user
+ WHEN:
+ - bulk_edit_object_perms API endpoint is called
+ THEN:
+ - User is not able to change permissions
+ """
+ self.t1.owner = User.objects.get(username="temp_admin")
+ self.t1.save()
+ self.client.force_authenticate(user=self.user1)
+
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id],
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+ self.assertEqual(response.content, b"Insufficient permissions")
+
+ def test_bulk_edit_object_permissions_validation(self):
+ """
+ GIVEN:
+ - Existing objects
+ WHEN:
+ - bulk_edit_object_perms API endpoint is called with invalid params
+ THEN:
+ - Validation fails
+ """
+ # not a list
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": self.t1.id,
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # not a list of ints
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": ["one"],
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # duplicates
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [self.t1.id, self.t2.id, self.t1.id],
+ "object_type": "tags",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # not a valid object type
+ response = self.client.post(
+ "/api/bulk_edit_object_perms/",
+ json.dumps(
+ {
+ "objects": [1],
+ "object_type": "madeup",
+ "owner": self.user1.id,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
--- /dev/null
+import json
+import urllib.request
+from unittest import mock
+from unittest.mock import MagicMock
+
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.tests.utils import DirectoriesMixin
+from paperless import version
+
+
+class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/remote_version/"
+
+ def setUp(self):
+ super().setUp()
+
+ @mock.patch("urllib.request.urlopen")
+ def test_remote_version_enabled_no_update_prefix(self, urlopen_mock):
+ cm = MagicMock()
+ cm.getcode.return_value = status.HTTP_200_OK
+ cm.read.return_value = json.dumps({"tag_name": "ngx-1.6.0"}).encode()
+ cm.__enter__.return_value = cm
+ urlopen_mock.return_value = cm
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertDictEqual(
+ response.data,
+ {
+ "version": "1.6.0",
+ "update_available": False,
+ },
+ )
+
+ @mock.patch("urllib.request.urlopen")
+ def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock):
+ cm = MagicMock()
+ cm.getcode.return_value = status.HTTP_200_OK
+ cm.read.return_value = json.dumps(
+ {"tag_name": version.__full_version_str__},
+ ).encode()
+ cm.__enter__.return_value = cm
+ urlopen_mock.return_value = cm
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertDictEqual(
+ response.data,
+ {
+ "version": version.__full_version_str__,
+ "update_available": False,
+ },
+ )
+
+ @mock.patch("urllib.request.urlopen")
+ def test_remote_version_enabled_update(self, urlopen_mock):
+ new_version = (
+ version.__version__[0],
+ version.__version__[1],
+ version.__version__[2] + 1,
+ )
+ new_version_str = ".".join(map(str, new_version))
+
+ cm = MagicMock()
+ cm.getcode.return_value = status.HTTP_200_OK
+ cm.read.return_value = json.dumps(
+ {"tag_name": new_version_str},
+ ).encode()
+ cm.__enter__.return_value = cm
+ urlopen_mock.return_value = cm
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertDictEqual(
+ response.data,
+ {
+ "version": new_version_str,
+ "update_available": True,
+ },
+ )
+
+ @mock.patch("urllib.request.urlopen")
+ def test_remote_version_bad_json(self, urlopen_mock):
+ cm = MagicMock()
+ cm.getcode.return_value = status.HTTP_200_OK
+ cm.read.return_value = b'{ "blah":'
+ cm.__enter__.return_value = cm
+ urlopen_mock.return_value = cm
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertDictEqual(
+ response.data,
+ {
+ "version": "0.0.0",
+ "update_available": False,
+ },
+ )
+
+ @mock.patch("urllib.request.urlopen")
+ def test_remote_version_exception(self, urlopen_mock):
+ cm = MagicMock()
+ cm.getcode.return_value = status.HTTP_200_OK
+ cm.read.side_effect = urllib.error.URLError("an error")
+ cm.__enter__.return_value = cm
+ urlopen_mock.return_value = cm
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertDictEqual(
+ response.data,
+ {
+ "version": "0.0.0",
+ "update_available": False,
+ },
+ )
--- /dev/null
+import uuid
+
+import celery
+from django.contrib.auth.models import User
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.models import PaperlessTask
+from documents.tests.utils import DirectoriesMixin
+
+
+class TestTasks(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/tasks/"
+ ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/"
+
+ def setUp(self):
+ super().setUp()
+
+ self.user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=self.user)
+
+ def test_get_tasks(self):
+ """
+ GIVEN:
+ - Attempted celery tasks
+ WHEN:
+ - API call is made to get tasks
+ THEN:
+ - Attempting and pending tasks are serialized and provided
+ """
+
+ task1 = PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_one.pdf",
+ )
+
+ task2 = PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_two.pdf",
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 2)
+ returned_task1 = response.data[1]
+ returned_task2 = response.data[0]
+
+ self.assertEqual(returned_task1["task_id"], task1.task_id)
+ self.assertEqual(returned_task1["status"], celery.states.PENDING)
+ self.assertEqual(returned_task1["task_file_name"], task1.task_file_name)
+
+ self.assertEqual(returned_task2["task_id"], task2.task_id)
+ self.assertEqual(returned_task2["status"], celery.states.PENDING)
+ self.assertEqual(returned_task2["task_file_name"], task2.task_file_name)
+
+ def test_get_single_task_status(self):
+ """
+ GIVEN
+ - Query parameter for a valid task ID
+ WHEN:
+ - API call is made to get task status
+ THEN:
+ - Single task data is returned
+ """
+
+ id1 = str(uuid.uuid4())
+ task1 = PaperlessTask.objects.create(
+ task_id=id1,
+ task_file_name="task_one.pdf",
+ )
+
+ _ = PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_two.pdf",
+ )
+
+ response = self.client.get(self.ENDPOINT + f"?task_id={id1}")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+ returned_task1 = response.data[0]
+
+ self.assertEqual(returned_task1["task_id"], task1.task_id)
+
+ def test_get_single_task_status_not_valid(self):
+ """
+ GIVEN
+ - Query parameter for a non-existent task ID
+ WHEN:
+ - API call is made to get task status
+ THEN:
+ - No task data is returned
+ """
+ PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_one.pdf",
+ )
+
+ _ = PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_two.pdf",
+ )
+
+ response = self.client.get(self.ENDPOINT + "?task_id=bad-task-id")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 0)
+
+ def test_acknowledge_tasks(self):
+ """
+ GIVEN:
+ - Attempted celery tasks
+ WHEN:
+ - API call is made to get mark task as acknowledged
+ THEN:
+ - Task is marked as acknowledged
+ """
+ task = PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_one.pdf",
+ )
+
+ response = self.client.get(self.ENDPOINT)
+ self.assertEqual(len(response.data), 1)
+
+ response = self.client.post(
+ self.ENDPOINT_ACKNOWLEDGE,
+ {"tasks": [task.id]},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ response = self.client.get(self.ENDPOINT)
+ self.assertEqual(len(response.data), 0)
+
+ def test_task_result_no_error(self):
+ """
+ GIVEN:
+ - A celery task completed without error
+ WHEN:
+ - API call is made to get tasks
+ THEN:
+ - The returned data includes the task result
+ """
+ PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_one.pdf",
+ status=celery.states.SUCCESS,
+ result="Success. New document id 1 created",
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ returned_data = response.data[0]
+
+ self.assertEqual(returned_data["result"], "Success. New document id 1 created")
+ self.assertEqual(returned_data["related_document"], "1")
+
+ def test_task_result_with_error(self):
+ """
+ GIVEN:
+ - A celery task completed with an exception
+ WHEN:
+ - API call is made to get tasks
+ THEN:
+ - The returned result is the exception info
+ """
+ PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="task_one.pdf",
+ status=celery.states.FAILURE,
+ result="test.pdf: Not consuming test.pdf: It is a duplicate.",
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ returned_data = response.data[0]
+
+ self.assertEqual(
+ returned_data["result"],
+ "test.pdf: Not consuming test.pdf: It is a duplicate.",
+ )
+
+ def test_task_name_webui(self):
+ """
+ GIVEN:
+ - Attempted celery task
+ - Task was created through the webui
+ WHEN:
+ - API call is made to get tasks
+ THEN:
+ - Returned data include the filename
+ """
+ PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="test.pdf",
+ task_name="documents.tasks.some_task",
+ status=celery.states.SUCCESS,
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ returned_data = response.data[0]
+
+ self.assertEqual(returned_data["task_file_name"], "test.pdf")
+
+ def test_task_name_consume_folder(self):
+ """
+ GIVEN:
+ - Attempted celery task
+ - Task was created through the consume folder
+ WHEN:
+ - API call is made to get tasks
+ THEN:
+ - Returned data include the filename
+ """
+ PaperlessTask.objects.create(
+ task_id=str(uuid.uuid4()),
+ task_file_name="anothertest.pdf",
+ task_name="documents.tasks.some_task",
+ status=celery.states.SUCCESS,
+ )
+
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ returned_data = response.data[0]
+
+ self.assertEqual(returned_data["task_file_name"], "anothertest.pdf")
--- /dev/null
+import json
+
+from django.contrib.auth.models import User
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.tests.utils import DirectoriesMixin
+
+
+class TestApiUiSettings(DirectoriesMixin, APITestCase):
+ ENDPOINT = "/api/ui_settings/"
+
+ def setUp(self):
+ super().setUp()
+ self.test_user = User.objects.create_superuser(username="test")
+ self.test_user.first_name = "Test"
+ self.test_user.last_name = "User"
+ self.test_user.save()
+ self.client.force_authenticate(user=self.test_user)
+
+ def test_api_get_ui_settings(self):
+ response = self.client.get(self.ENDPOINT, format="json")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertDictEqual(
+ response.data["user"],
+ {
+ "id": self.test_user.id,
+ "username": self.test_user.username,
+ "is_superuser": True,
+ "groups": [],
+ "first_name": self.test_user.first_name,
+ "last_name": self.test_user.last_name,
+ },
+ )
+ self.assertDictEqual(
+ response.data["settings"],
+ {
+ "update_checking": {
+ "backend_setting": "default",
+ },
+ },
+ )
+
+ def test_api_set_ui_settings(self):
+ settings = {
+ "settings": {
+ "dark_mode": {
+ "enabled": True,
+ },
+ },
+ }
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(settings),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ ui_settings = self.test_user.ui_settings
+ self.assertDictEqual(
+ ui_settings.settings,
+ settings["settings"],
+ )