mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge pull request #1375 from tim-vogel/add_comments
Feature: document comments
This commit is contained in:
@@ -12,6 +12,7 @@ from django.core import serializers
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import CommandError
|
||||
from django.db import transaction
|
||||
from documents.models import Comment
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
@@ -126,6 +127,10 @@ class Command(BaseCommand):
|
||||
serializers.serialize("json", DocumentType.objects.all()),
|
||||
)
|
||||
|
||||
manifest += json.loads(
|
||||
serializers.serialize("json", Comment.objects.all()),
|
||||
)
|
||||
|
||||
documents = Document.objects.order_by("id")
|
||||
document_map = {d.pk: d for d in documents}
|
||||
document_manifest = json.loads(serializers.serialize("json", documents))
|
||||
|
28
src/documents/migrations/1023_add_comments.py
Normal file
28
src/documents/migrations/1023_add_comments.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1022_paperlesstask"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Comment",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("comment", models.TextField()),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("document_id", models.PositiveIntegerField()),
|
||||
("user_id", models.PositiveIntegerField()),
|
||||
],
|
||||
)
|
||||
]
|
13
src/documents/migrations/1024_merge_20220824_1341.py
Normal file
13
src/documents/migrations/1024_merge_20220824_1341.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Generated by Django 4.0.6 on 2022-08-24 13:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("documents", "1023_add_comments"),
|
||||
("documents", "1023_document_original_filename"),
|
||||
]
|
||||
|
||||
operations = []
|
@@ -537,3 +537,43 @@ class PaperlessTask(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
acknowledged = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
comment = models.TextField(
|
||||
_("content"),
|
||||
blank=True,
|
||||
help_text=_("Comment for the document"),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(
|
||||
_("created"),
|
||||
default=timezone.now,
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
document = models.ForeignKey(
|
||||
Document,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="documents",
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("document"),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="users",
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("created",)
|
||||
verbose_name = _("comment")
|
||||
verbose_name_plural = _("comments")
|
||||
|
||||
def __str__(self):
|
||||
return self.content
|
||||
|
@@ -32,6 +32,7 @@ from documents.models import SavedView
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import UiSettings
|
||||
from documents.models import Comment
|
||||
from documents.models import StoragePath
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from paperless import version
|
||||
@@ -1357,6 +1358,133 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
||||
1,
|
||||
)
|
||||
|
||||
def test_get_existing_comments(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- A document with a single comment
|
||||
WHEN:
|
||||
- API reuqest for document comments is made
|
||||
THEN:
|
||||
- The associated comment is returned
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
mime_type="application/pdf",
|
||||
content="this is a document which will have comments!",
|
||||
)
|
||||
comment = Comment.objects.create(
|
||||
comment="This is a comment.",
|
||||
document=doc,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/documents/{doc.pk}/comments/",
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
self.assertEqual(len(resp_data), 1)
|
||||
|
||||
resp_data = resp_data[0]
|
||||
del resp_data["created"]
|
||||
|
||||
self.assertDictEqual(
|
||||
resp_data,
|
||||
{
|
||||
"id": comment.id,
|
||||
"comment": comment.comment,
|
||||
"user": {
|
||||
"id": comment.user.id,
|
||||
"username": comment.user.username,
|
||||
"firstname": comment.user.first_name,
|
||||
"lastname": comment.user.last_name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_create_comment(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing document
|
||||
WHEN:
|
||||
- API request is made to add a comment
|
||||
THEN:
|
||||
- Comment is created and associated with document
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
mime_type="application/pdf",
|
||||
content="this is a document which will have comments added",
|
||||
)
|
||||
resp = self.client.post(
|
||||
f"/api/documents/{doc.pk}/comments/",
|
||||
data={"comment": "this is a posted comment"},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/documents/{doc.pk}/comments/",
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
resp_data = response.json()
|
||||
|
||||
self.assertEqual(len(resp_data), 1)
|
||||
|
||||
resp_data = resp_data[0]
|
||||
|
||||
self.assertEqual(resp_data["comment"], "this is a posted comment")
|
||||
|
||||
def test_delete_comment(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing document
|
||||
WHEN:
|
||||
- API request is made to add a comment
|
||||
THEN:
|
||||
- Comment is created and associated with document
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
mime_type="application/pdf",
|
||||
content="this is a document which will have comments!",
|
||||
)
|
||||
comment = Comment.objects.create(
|
||||
comment="This is a comment.",
|
||||
document=doc,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
response = self.client.delete(
|
||||
f"/api/documents/{doc.pk}/comments/?id={comment.pk}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertEqual(len(Comment.objects.all()), 0)
|
||||
|
||||
def test_get_comments_no_doc(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- A request to get comments from a non-existent document
|
||||
WHEN:
|
||||
- API request for document comments is made
|
||||
THEN:
|
||||
- HTTP 404 is returned
|
||||
"""
|
||||
response = self.client.get(
|
||||
"/api/documents/500/comments/",
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
class TestDocumentApiV2(DirectoriesMixin, APITestCase):
|
||||
def setUp(self):
|
||||
|
@@ -10,10 +10,12 @@ from django.core.management import call_command
|
||||
from django.test import override_settings
|
||||
from django.test import TestCase
|
||||
from documents.management.commands import document_exporter
|
||||
from documents.models import Comment
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import Tag
|
||||
from documents.models import User
|
||||
from documents.sanity_checker import check_sanity
|
||||
from documents.settings import EXPORTER_FILE_NAME
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
@@ -25,6 +27,8 @@ class TestExportImport(DirectoriesMixin, TestCase):
|
||||
self.target = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, self.target)
|
||||
|
||||
self.user = User.objects.create(username="temp_admin")
|
||||
|
||||
self.d1 = Document.objects.create(
|
||||
content="Content",
|
||||
checksum="42995833e01aea9b3edee44bbfdd7ce1",
|
||||
@@ -57,6 +61,12 @@ class TestExportImport(DirectoriesMixin, TestCase):
|
||||
storage_type=Document.STORAGE_TYPE_GPG,
|
||||
)
|
||||
|
||||
self.comment = Comment.objects.create(
|
||||
comment="This is a comment. amaze.",
|
||||
document=self.d1,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.t1 = Tag.objects.create(name="t")
|
||||
self.dt1 = DocumentType.objects.create(name="dt")
|
||||
self.c1 = Correspondent.objects.create(name="c")
|
||||
@@ -110,7 +120,7 @@ class TestExportImport(DirectoriesMixin, TestCase):
|
||||
|
||||
manifest = self._do_export(use_filename_format=use_filename_format)
|
||||
|
||||
self.assertEqual(len(manifest), 8)
|
||||
self.assertEqual(len(manifest), 10)
|
||||
self.assertEqual(
|
||||
len(list(filter(lambda e: e["model"] == "documents.document", manifest))),
|
||||
4,
|
||||
@@ -171,6 +181,11 @@ class TestExportImport(DirectoriesMixin, TestCase):
|
||||
checksum = hashlib.md5(f.read()).hexdigest()
|
||||
self.assertEqual(checksum, element["fields"]["archive_checksum"])
|
||||
|
||||
elif element["model"] == "documents.comment":
|
||||
self.assertEqual(element["fields"]["comment"], self.comment.comment)
|
||||
self.assertEqual(element["fields"]["document"], self.d1.id)
|
||||
self.assertEqual(element["fields"]["user"], self.user.id)
|
||||
|
||||
with paperless_environment() as dirs:
|
||||
self.assertEqual(Document.objects.count(), 4)
|
||||
Document.objects.all().delete()
|
||||
|
@@ -64,6 +64,7 @@ from .matching import match_correspondents
|
||||
from .matching import match_document_types
|
||||
from .matching import match_storage_paths
|
||||
from .matching import match_tags
|
||||
from .models import Comment
|
||||
from .models import Correspondent
|
||||
from .models import Document
|
||||
from .models import DocumentType
|
||||
@@ -387,6 +388,67 @@ class DocumentViewSet(
|
||||
except (FileNotFoundError, Document.DoesNotExist):
|
||||
raise Http404()
|
||||
|
||||
def getComments(self, doc):
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"comment": c.comment,
|
||||
"created": c.created,
|
||||
"user": {
|
||||
"id": c.user.id,
|
||||
"username": c.user.username,
|
||||
"firstname": c.user.first_name,
|
||||
"lastname": c.user.last_name,
|
||||
},
|
||||
}
|
||||
for c in Comment.objects.filter(document=doc).order_by("-created")
|
||||
]
|
||||
|
||||
@action(methods=["get", "post", "delete"], detail=True)
|
||||
def comments(self, request, pk=None):
|
||||
try:
|
||||
doc = Document.objects.get(pk=pk)
|
||||
except Document.DoesNotExist:
|
||||
raise Http404()
|
||||
|
||||
currentUser = request.user
|
||||
|
||||
if request.method == "GET":
|
||||
try:
|
||||
return Response(self.getComments(doc))
|
||||
except Exception as e:
|
||||
logger.warning(f"An error occurred retrieving comments: {str(e)}")
|
||||
return Response(
|
||||
{"error": "Error retreiving comments, check logs for more detail."},
|
||||
)
|
||||
elif request.method == "POST":
|
||||
try:
|
||||
c = Comment.objects.create(
|
||||
document=doc,
|
||||
comment=request.data["comment"],
|
||||
user=currentUser,
|
||||
)
|
||||
c.save()
|
||||
|
||||
return Response(self.getComments(doc))
|
||||
except Exception as e:
|
||||
logger.warning(f"An error occurred saving comment: {str(e)}")
|
||||
return Response(
|
||||
{
|
||||
"error": "Error saving comment, check logs for more detail.",
|
||||
},
|
||||
)
|
||||
elif request.method == "DELETE":
|
||||
comment = Comment.objects.get(id=int(request.GET.get("id")))
|
||||
comment.delete()
|
||||
return Response(self.getComments(doc))
|
||||
|
||||
return Response(
|
||||
{
|
||||
"error": "error",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SearchResultSerializer(DocumentSerializer):
|
||||
def to_representation(self, instance):
|
||||
|
Reference in New Issue
Block a user