mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge pull request #2904 from paperless-ngx/feature-improve-comments-ui
Enhancement: rename comments to notes and improve notes UI
This commit is contained in:
@@ -4,6 +4,7 @@ from guardian.admin import GuardedModelAdmin
|
||||
from .models import Correspondent
|
||||
from .models import Document
|
||||
from .models import DocumentType
|
||||
from .models import Note
|
||||
from .models import PaperlessTask
|
||||
from .models import SavedView
|
||||
from .models import SavedViewFilterRule
|
||||
@@ -131,6 +132,13 @@ class TaskAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
class NotesAdmin(GuardedModelAdmin):
|
||||
|
||||
list_display = ("user", "created", "note", "document")
|
||||
list_filter = ("created", "user")
|
||||
list_display_links = ("created",)
|
||||
|
||||
|
||||
admin.site.register(Correspondent, CorrespondentAdmin)
|
||||
admin.site.register(Tag, TagAdmin)
|
||||
admin.site.register(DocumentType, DocumentTypeAdmin)
|
||||
@@ -138,3 +146,4 @@ admin.site.register(Document, DocumentAdmin)
|
||||
admin.site.register(SavedView, SavedViewAdmin)
|
||||
admin.site.register(StoragePath, StoragePathAdmin)
|
||||
admin.site.register(PaperlessTask, TaskAdmin)
|
||||
admin.site.register(Note, NotesAdmin)
|
||||
|
@@ -6,8 +6,8 @@ from contextlib import contextmanager
|
||||
from dateutil.parser import isoparse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from documents.models import Comment
|
||||
from documents.models import Document
|
||||
from documents.models import Note
|
||||
from guardian.shortcuts import get_users_with_perms
|
||||
from whoosh import classify
|
||||
from whoosh import highlight
|
||||
@@ -52,7 +52,7 @@ def get_schema():
|
||||
path=TEXT(sortable=True),
|
||||
path_id=NUMERIC(),
|
||||
has_path=BOOLEAN(),
|
||||
comments=TEXT(),
|
||||
notes=TEXT(),
|
||||
owner=TEXT(),
|
||||
owner_id=NUMERIC(),
|
||||
has_owner=BOOLEAN(),
|
||||
@@ -98,7 +98,7 @@ def open_index_searcher():
|
||||
def update_document(writer: AsyncWriter, doc: Document):
|
||||
tags = ",".join([t.name for t in doc.tags.all()])
|
||||
tags_ids = ",".join([str(t.id) for t in doc.tags.all()])
|
||||
comments = ",".join([str(c.comment) for c in Comment.objects.filter(document=doc)])
|
||||
notes = ",".join([str(c.note) for c in Note.objects.filter(document=doc)])
|
||||
asn = doc.archive_serial_number
|
||||
if asn is not None and (
|
||||
asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||
@@ -136,7 +136,7 @@ def update_document(writer: AsyncWriter, doc: Document):
|
||||
path=doc.storage_path.name if doc.storage_path else None,
|
||||
path_id=doc.storage_path.id if doc.storage_path else None,
|
||||
has_path=doc.storage_path is not None,
|
||||
comments=comments,
|
||||
notes=notes,
|
||||
owner=doc.owner.username if doc.owner else None,
|
||||
owner_id=doc.owner.id if doc.owner else None,
|
||||
has_owner=doc.owner is not None,
|
||||
@@ -293,7 +293,7 @@ class DelayedFullTextQuery(DelayedQuery):
|
||||
def _get_query(self):
|
||||
q_str = self.query_params["query"]
|
||||
qp = MultifieldParser(
|
||||
["content", "title", "correspondent", "tag", "type", "comments"],
|
||||
["content", "title", "correspondent", "tag", "type", "notes"],
|
||||
self.searcher.ixreader.schema,
|
||||
)
|
||||
qp.add_plugin(DateParserPlugin(basedate=timezone.now()))
|
||||
|
@@ -17,10 +17,10 @@ from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from documents.models import Comment
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import Note
|
||||
from documents.models import SavedView
|
||||
from documents.models import SavedViewFilterRule
|
||||
from documents.models import StoragePath
|
||||
@@ -206,7 +206,7 @@ class Command(BaseCommand):
|
||||
self.files_in_export_dir.add(x.resolve())
|
||||
|
||||
# 2. Create manifest, containing all correspondents, types, tags, storage paths
|
||||
# comments, documents and ui_settings
|
||||
# note, documents and ui_settings
|
||||
with transaction.atomic():
|
||||
manifest = json.loads(
|
||||
serializers.serialize("json", Correspondent.objects.all()),
|
||||
@@ -222,11 +222,11 @@ class Command(BaseCommand):
|
||||
serializers.serialize("json", StoragePath.objects.all()),
|
||||
)
|
||||
|
||||
comments = json.loads(
|
||||
serializers.serialize("json", Comment.objects.all()),
|
||||
notes = json.loads(
|
||||
serializers.serialize("json", Note.objects.all()),
|
||||
)
|
||||
if not self.split_manifest:
|
||||
manifest += comments
|
||||
manifest += notes
|
||||
|
||||
documents = Document.objects.order_by("id")
|
||||
document_map = {d.pk: d for d in documents}
|
||||
@@ -359,7 +359,7 @@ class Command(BaseCommand):
|
||||
content += list(
|
||||
filter(
|
||||
lambda d: d["fields"]["document"] == document_dict["pk"],
|
||||
comments,
|
||||
notes,
|
||||
),
|
||||
)
|
||||
manifest_name.write_text(json.dumps(content, indent=2))
|
||||
|
61
src/documents/migrations/1035_rename_comment_note.py
Normal file
61
src/documents/migrations/1035_rename_comment_note.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 4.1.5 on 2023-03-17 22:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("documents", "1034_alter_savedviewfilterrule_rule_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="Comment",
|
||||
new_name="Note",
|
||||
),
|
||||
migrations.RenameField(model_name="note", old_name="comment", new_name="note"),
|
||||
migrations.AlterModelOptions(
|
||||
name="note",
|
||||
options={
|
||||
"ordering": ("created",),
|
||||
"verbose_name": "note",
|
||||
"verbose_name_plural": "notes",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="note",
|
||||
name="document",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="notes",
|
||||
to="documents.document",
|
||||
verbose_name="document",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="note",
|
||||
name="note",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Note for the document", verbose_name="content"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="note",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="notes",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="user",
|
||||
),
|
||||
),
|
||||
]
|
@@ -635,11 +635,11 @@ class PaperlessTask(models.Model):
|
||||
)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
comment = models.TextField(
|
||||
class Note(models.Model):
|
||||
note = models.TextField(
|
||||
_("content"),
|
||||
blank=True,
|
||||
help_text=_("Comment for the document"),
|
||||
help_text=_("Note for the document"),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(
|
||||
@@ -652,7 +652,7 @@ class Comment(models.Model):
|
||||
Document,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="documents",
|
||||
related_name="notes",
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("document"),
|
||||
)
|
||||
@@ -661,15 +661,15 @@ class Comment(models.Model):
|
||||
User,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="users",
|
||||
related_name="notes",
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("created",)
|
||||
verbose_name = _("comment")
|
||||
verbose_name_plural = _("comments")
|
||||
verbose_name = _("note")
|
||||
verbose_name_plural = _("notes")
|
||||
|
||||
def __str__(self):
|
||||
return self.content
|
||||
return self.note
|
||||
|
@@ -442,6 +442,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer):
|
||||
"owner",
|
||||
"permissions",
|
||||
"set_permissions",
|
||||
"notes",
|
||||
)
|
||||
|
||||
|
||||
|
@@ -38,7 +38,7 @@ from documents.models import PaperlessTask
|
||||
from documents.models import SavedView
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import Comment
|
||||
from documents.models import Note
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from paperless import version
|
||||
from rest_framework.test import APITestCase
|
||||
@@ -1717,28 +1717,28 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
||||
1,
|
||||
)
|
||||
|
||||
def test_get_existing_comments(self):
|
||||
def test_get_existing_notes(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- A document with a single comment
|
||||
- A document with a single note
|
||||
WHEN:
|
||||
- API reuqest for document comments is made
|
||||
- API reuqest for document notes is made
|
||||
THEN:
|
||||
- The associated comment is returned
|
||||
- The associated note is returned
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="test",
|
||||
mime_type="application/pdf",
|
||||
content="this is a document which will have comments!",
|
||||
content="this is a document which will have notes!",
|
||||
)
|
||||
comment = Comment.objects.create(
|
||||
comment="This is a comment.",
|
||||
note = Note.objects.create(
|
||||
note="This is a note.",
|
||||
document=doc,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/documents/{doc.pk}/comments/",
|
||||
f"/api/documents/{doc.pk}/notes/",
|
||||
format="json",
|
||||
)
|
||||
|
||||
@@ -1754,39 +1754,39 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
||||
self.assertDictEqual(
|
||||
resp_data,
|
||||
{
|
||||
"id": comment.id,
|
||||
"comment": comment.comment,
|
||||
"id": note.id,
|
||||
"note": note.note,
|
||||
"user": {
|
||||
"id": comment.user.id,
|
||||
"username": comment.user.username,
|
||||
"first_name": comment.user.first_name,
|
||||
"last_name": comment.user.last_name,
|
||||
"id": note.user.id,
|
||||
"username": note.user.username,
|
||||
"first_name": note.user.first_name,
|
||||
"last_name": note.user.last_name,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_create_comment(self):
|
||||
def test_create_note(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing document
|
||||
WHEN:
|
||||
- API request is made to add a comment
|
||||
- API request is made to add a note
|
||||
THEN:
|
||||
- Comment is created and associated with document
|
||||
- note 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",
|
||||
content="this is a document which will have notes added",
|
||||
)
|
||||
resp = self.client.post(
|
||||
f"/api/documents/{doc.pk}/comments/",
|
||||
data={"comment": "this is a posted comment"},
|
||||
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}/comments/",
|
||||
f"/api/documents/{doc.pk}/notes/",
|
||||
format="json",
|
||||
)
|
||||
|
||||
@@ -1798,48 +1798,48 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
||||
|
||||
resp_data = resp_data[0]
|
||||
|
||||
self.assertEqual(resp_data["comment"], "this is a posted comment")
|
||||
self.assertEqual(resp_data["note"], "this is a posted note")
|
||||
|
||||
def test_delete_comment(self):
|
||||
def test_delete_note(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing document
|
||||
WHEN:
|
||||
- API request is made to add a comment
|
||||
- API request is made to add a note
|
||||
THEN:
|
||||
- Comment is created and associated with document
|
||||
- note 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!",
|
||||
content="this is a document which will have notes!",
|
||||
)
|
||||
comment = Comment.objects.create(
|
||||
comment="This is a comment.",
|
||||
note = Note.objects.create(
|
||||
note="This is a note.",
|
||||
document=doc,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
response = self.client.delete(
|
||||
f"/api/documents/{doc.pk}/comments/?id={comment.pk}",
|
||||
f"/api/documents/{doc.pk}/notes/?id={note.pk}",
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
self.assertEqual(len(Comment.objects.all()), 0)
|
||||
self.assertEqual(len(Note.objects.all()), 0)
|
||||
|
||||
def test_get_comments_no_doc(self):
|
||||
def test_get_notes_no_doc(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- A request to get comments from a non-existent document
|
||||
- A request to get notes from a non-existent document
|
||||
WHEN:
|
||||
- API request for document comments is made
|
||||
- API request for document notes is made
|
||||
THEN:
|
||||
- HTTP status.HTTP_404_NOT_FOUND is returned
|
||||
"""
|
||||
response = self.client.get(
|
||||
"/api/documents/500/comments/",
|
||||
"/api/documents/500/notes/",
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
@@ -13,10 +13,10 @@ from django.test import override_settings
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
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 Note
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import User
|
||||
@@ -66,8 +66,8 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
storage_type=Document.STORAGE_TYPE_GPG,
|
||||
)
|
||||
|
||||
self.comment = Comment.objects.create(
|
||||
comment="This is a comment. amaze.",
|
||||
self.note = Note.objects.create(
|
||||
note="This is a note. amaze.",
|
||||
document=self.d1,
|
||||
user=self.user,
|
||||
)
|
||||
@@ -199,8 +199,8 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, 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)
|
||||
elif element["model"] == "documents.note":
|
||||
self.assertEqual(element["fields"]["note"], self.note.note)
|
||||
self.assertEqual(element["fields"]["document"], self.d1.id)
|
||||
self.assertEqual(element["fields"]["user"], self.user.id)
|
||||
|
||||
|
@@ -72,10 +72,10 @@ 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
|
||||
from .models import Note
|
||||
from .models import PaperlessTask
|
||||
from .models import SavedView
|
||||
from .models import StoragePath
|
||||
@@ -230,7 +230,7 @@ class DocumentViewSet(
|
||||
GenericViewSet,
|
||||
):
|
||||
model = Document
|
||||
queryset = Document.objects.all()
|
||||
queryset = Document.objects.annotate(num_notes=Count("notes"))
|
||||
serializer_class = DocumentSerializer
|
||||
pagination_class = StandardPagination
|
||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||
@@ -251,10 +251,11 @@ class DocumentViewSet(
|
||||
"modified",
|
||||
"added",
|
||||
"archive_serial_number",
|
||||
"num_notes",
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return Document.objects.distinct()
|
||||
return Document.objects.distinct().annotate(num_notes=Count("notes"))
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
super().get_serializer(*args, **kwargs)
|
||||
@@ -441,11 +442,11 @@ class DocumentViewSet(
|
||||
except (FileNotFoundError, Document.DoesNotExist):
|
||||
raise Http404()
|
||||
|
||||
def getComments(self, doc):
|
||||
def getNotes(self, doc):
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"comment": c.comment,
|
||||
"note": c.note,
|
||||
"created": c.created,
|
||||
"user": {
|
||||
"id": c.user.id,
|
||||
@@ -454,11 +455,11 @@ class DocumentViewSet(
|
||||
"last_name": c.user.last_name,
|
||||
},
|
||||
}
|
||||
for c in Comment.objects.filter(document=doc).order_by("-created")
|
||||
for c in Note.objects.filter(document=doc).order_by("-created")
|
||||
]
|
||||
|
||||
@action(methods=["get", "post", "delete"], detail=True)
|
||||
def comments(self, request, pk=None):
|
||||
def notes(self, request, pk=None):
|
||||
try:
|
||||
doc = Document.objects.get(pk=pk)
|
||||
except Document.DoesNotExist:
|
||||
@@ -468,17 +469,17 @@ class DocumentViewSet(
|
||||
|
||||
if request.method == "GET":
|
||||
try:
|
||||
return Response(self.getComments(doc))
|
||||
return Response(self.getNotes(doc))
|
||||
except Exception as e:
|
||||
logger.warning(f"An error occurred retrieving comments: {str(e)}")
|
||||
logger.warning(f"An error occurred retrieving notes: {str(e)}")
|
||||
return Response(
|
||||
{"error": "Error retreiving comments, check logs for more detail."},
|
||||
{"error": "Error retreiving notes, check logs for more detail."},
|
||||
)
|
||||
elif request.method == "POST":
|
||||
try:
|
||||
c = Comment.objects.create(
|
||||
c = Note.objects.create(
|
||||
document=doc,
|
||||
comment=request.data["comment"],
|
||||
note=request.data["note"],
|
||||
user=currentUser,
|
||||
)
|
||||
c.save()
|
||||
@@ -487,23 +488,23 @@ class DocumentViewSet(
|
||||
|
||||
index.add_or_update_document(self.get_object())
|
||||
|
||||
return Response(self.getComments(doc))
|
||||
return Response(self.getNotes(doc))
|
||||
except Exception as e:
|
||||
logger.warning(f"An error occurred saving comment: {str(e)}")
|
||||
logger.warning(f"An error occurred saving note: {str(e)}")
|
||||
return Response(
|
||||
{
|
||||
"error": "Error saving comment, check logs for more detail.",
|
||||
"error": "Error saving note, check logs for more detail.",
|
||||
},
|
||||
)
|
||||
elif request.method == "DELETE":
|
||||
comment = Comment.objects.get(id=int(request.GET.get("id")))
|
||||
comment.delete()
|
||||
note = Note.objects.get(id=int(request.GET.get("id")))
|
||||
note.delete()
|
||||
|
||||
from documents import index
|
||||
|
||||
index.add_or_update_document(self.get_object())
|
||||
|
||||
return Response(self.getComments(doc))
|
||||
return Response(self.getNotes(doc))
|
||||
|
||||
return Response(
|
||||
{
|
||||
@@ -515,14 +516,14 @@ class DocumentViewSet(
|
||||
class SearchResultSerializer(DocumentSerializer, PassUserMixin):
|
||||
def to_representation(self, instance):
|
||||
doc = Document.objects.get(id=instance["id"])
|
||||
comments = ",".join(
|
||||
[str(c.comment) for c in Comment.objects.filter(document=instance["id"])],
|
||||
notes = ",".join(
|
||||
[str(c.note) for c in Note.objects.filter(document=instance["id"])],
|
||||
)
|
||||
r = super().to_representation(doc)
|
||||
r["__search_hit__"] = {
|
||||
"score": instance.score,
|
||||
"highlights": instance.highlights("content", text=doc.content),
|
||||
"comment_highlights": instance.highlights("comments", text=comments)
|
||||
"note_highlights": instance.highlights("notes", text=notes)
|
||||
if doc
|
||||
else None,
|
||||
"rank": instance.rank,
|
||||
|
Reference in New Issue
Block a user