From fdcbf9c75c6c6fa360841e7472a46140fd4aaca3 Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Sun, 6 Dec 2020 23:30:51 +0100 Subject: [PATCH 01/37] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd6080d35..a8fb1f8e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,3 +24,7 @@ feature-X branches is for experimental stuff that will eventually be merged into I'm trying to get most of paperless tested, so please do the same for your code! I know its a hassle, but it makes sure that your code works now and will allow us to detect regressions easily. To test your code, execute `pytest` in the src/ directory. Executing that in the project root is no good. This also generates a html coverage report, which you can use to see if you missed anything important during testing. + +## More info: + +... is available in the documentation. https://paperless-ng.readthedocs.io/en/latest/extending.html From 1357ee83a0485268403eb8e8081e9936aaed8180 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Tue, 8 Dec 2020 21:20:05 +0100 Subject: [PATCH 02/37] version bump --- docker/hub/docker-compose.postgres.yml | 2 +- docker/hub/docker-compose.sqlite.yml | 2 +- src/paperless/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml index 295d981e1..24f0e118f 100644 --- a/docker/hub/docker-compose.postgres.yml +++ b/docker/hub/docker-compose.postgres.yml @@ -15,7 +15,7 @@ services: POSTGRES_PASSWORD: paperless webserver: - image: jonaswinkler/paperless-ng:0.9.5 + image: jonaswinkler/paperless-ng:0.9.6 restart: always depends_on: - db diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml index 80df40596..6ae619fd6 100644 --- a/docker/hub/docker-compose.sqlite.yml +++ b/docker/hub/docker-compose.sqlite.yml @@ -5,7 +5,7 @@ services: restart: always webserver: - image: jonaswinkler/paperless-ng:0.9.5 + image: jonaswinkler/paperless-ng:0.9.6 restart: always depends_on: - broker diff --git a/src/paperless/version.py b/src/paperless/version.py index 26e46fea8..527e0668d 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1 +1 @@ -__version__ = (0, 9, 5) +__version__ = (0, 9, 6) From ba7bf9b2d2d77643cbb30f0aa4f138a6de7befca Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 9 Dec 2020 00:04:37 +0100 Subject: [PATCH 03/37] removed slugs entirely, since their only purpose was purely cosmetic anyway. --- src/documents/admin.py | 8 +----- src/documents/consumer.py | 2 +- .../management/commands/document_consumer.py | 5 +--- .../migrations/1006_auto_20201208_2209.py | 25 +++++++++++++++++++ src/documents/models.py | 11 ++------ src/documents/serialisers.py | 19 +++++++++++--- src/documents/signals/handlers.py | 4 +-- src/documents/tests/test_consumer.py | 12 ++++----- src/paperless_mail/mail.py | 5 +--- 9 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 src/documents/migrations/1006_auto_20201208_2209.py diff --git a/src/documents/admin.py b/src/documents/admin.py index 2a4fb0031..055a6fd93 100755 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -17,8 +17,6 @@ class CorrespondentAdmin(admin.ModelAdmin): list_filter = ("matching_algorithm",) list_editable = ("match", "matching_algorithm") - readonly_fields = ("slug",) - class TagAdmin(admin.ModelAdmin): @@ -31,8 +29,6 @@ class TagAdmin(admin.ModelAdmin): list_filter = ("colour", "matching_algorithm") list_editable = ("colour", "match", "matching_algorithm") - readonly_fields = ("slug", ) - class DocumentTypeAdmin(admin.ModelAdmin): @@ -44,8 +40,6 @@ class DocumentTypeAdmin(admin.ModelAdmin): list_filter = ("matching_algorithm",) list_editable = ("match", "matching_algorithm") - readonly_fields = ("slug",) - class DocumentAdmin(admin.ModelAdmin): @@ -106,7 +100,7 @@ class DocumentAdmin(admin.ModelAdmin): for tag in obj.tags.all(): r += self._html_tag( "span", - tag.slug + ", " + tag.name + ", " ) return r diff --git a/src/documents/consumer.py b/src/documents/consumer.py index f52dd5a7d..19ca3ed7e 100755 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -259,7 +259,7 @@ class Consumer(LoggingMixin): relevant_tags = set(file_info.tags) if relevant_tags: - tag_names = ", ".join([t.slug for t in relevant_tags]) + tag_names = ", ".join([t.name for t in relevant_tags]) self.log("debug", "Tagging with {}".format(tag_names)) document.tags.add(*relevant_tags) diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 5cecd6bf9..b2f689aed 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -29,10 +29,7 @@ def _tags_from_path(filepath): path_parts = Path(filepath).relative_to( settings.CONSUMPTION_DIR).parent.parts for part in path_parts: - tag_ids.add(Tag.objects.get_or_create( - slug=slugify(part), - defaults={"name": part}, - )[0].pk) + tag_ids.add(Tag.objects.get_or_create(name=part)[0].pk) return tag_ids diff --git a/src/documents/migrations/1006_auto_20201208_2209.py b/src/documents/migrations/1006_auto_20201208_2209.py new file mode 100644 index 000000000..49f8c8dfe --- /dev/null +++ b/src/documents/migrations/1006_auto_20201208_2209.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.4 on 2020-12-08 22:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '1005_checksums'), + ] + + operations = [ + migrations.RemoveField( + model_name='correspondent', + name='slug', + ), + migrations.RemoveField( + model_name='documenttype', + name='slug', + ), + migrations.RemoveField( + model_name='tag', + name='slug', + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 366cb215d..f0678a843 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -36,7 +36,6 @@ class MatchingModel(models.Model): ) name = models.CharField(max_length=128, unique=True) - slug = models.SlugField(blank=True, editable=False) match = models.CharField(max_length=256, blank=True) matching_algorithm = models.PositiveIntegerField( @@ -69,7 +68,6 @@ class MatchingModel(models.Model): def save(self, *args, **kwargs): self.match = self.match.lower() - self.slug = slugify(self.name) models.Model.save(self, *args, **kwargs) @@ -384,9 +382,7 @@ class FileInfo: def _get_correspondent(cls, name): if not name: return None - return Correspondent.objects.get_or_create(name=name, defaults={ - "slug": slugify(name) - })[0] + return Correspondent.objects.get_or_create(name=name)[0] @classmethod def _get_title(cls, title): @@ -396,10 +392,7 @@ class FileInfo: def _get_tags(cls, tags): r = [] for t in tags.split(","): - r.append(Tag.objects.get_or_create( - slug=slugify(t), - defaults={"name": t} - )[0]) + r.append(Tag.objects.get_or_create(name=t)[0]) return tuple(r) @classmethod diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 5aedeeb58..600645061 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1,4 +1,5 @@ import magic +from django.utils.text import slugify from pathvalidate import validate_filename, ValidationError from rest_framework import serializers from rest_framework.fields import SerializerMethodField @@ -7,12 +8,16 @@ from .models import Correspondent, Tag, Document, Log, DocumentType from .parsers import is_mime_type_supported -class CorrespondentSerializer(serializers.HyperlinkedModelSerializer): +class CorrespondentSerializer(serializers.ModelSerializer): document_count = serializers.IntegerField(read_only=True) last_correspondence = serializers.DateTimeField(read_only=True) + def get_slug(self, obj): + return slugify(obj.name) + slug = SerializerMethodField() + class Meta: model = Correspondent fields = ( @@ -27,10 +32,14 @@ class CorrespondentSerializer(serializers.HyperlinkedModelSerializer): ) -class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): +class DocumentTypeSerializer(serializers.ModelSerializer): document_count = serializers.IntegerField(read_only=True) + def get_slug(self, obj): + return slugify(obj.name) + slug = SerializerMethodField() + class Meta: model = DocumentType fields = ( @@ -44,10 +53,14 @@ class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): ) -class TagSerializer(serializers.HyperlinkedModelSerializer): +class TagSerializer(serializers.ModelSerializer): document_count = serializers.IntegerField(read_only=True) + def get_slug(self, obj): + return slugify(obj.name) + slug = SerializerMethodField() + class Meta: model = Tag fields = ( diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 8a9ce18d7..8121072bf 100755 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -136,7 +136,7 @@ def set_tags(sender, message = 'Tagging "{}" with "{}"' logger( - message.format(document, ", ".join([t.slug for t in relevant_tags])), + message.format(document, ", ".join([t.name for t in relevant_tags])), logging_group ) @@ -165,7 +165,7 @@ def run_post_consume_script(sender, document, **kwargs): reverse("document-download", kwargs={"pk": document.pk}), reverse("document-thumb", kwargs={"pk": document.pk}), str(document.correspondent), - str(",".join(document.tags.all().values_list("slug", flat=True))) + str(",".join(document.tags.all().values_list("name", flat=True))) )).wait() diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index f828d3e11..b4b19be4c 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -27,7 +27,7 @@ class TestAttributes(TestCase): self.assertEqual(file_info.title, title, filename) - self.assertEqual(tuple([t.slug for t in file_info.tags]), tags, filename) + self.assertEqual(tuple([t.name for t in file_info.tags]), tags, filename) def test_guess_attributes_from_name0(self): self._test_guess_attributes_from_name( @@ -188,7 +188,7 @@ class TestFieldPermutations(TestCase): self.assertEqual(info.tags, (), filename) else: self.assertEqual( - [t.slug for t in info.tags], tags.split(','), + [t.name for t in info.tags], tags.split(','), filename ) @@ -342,8 +342,8 @@ class TestFieldPermutations(TestCase): info = FileInfo.from_filename(filename) self.assertEqual(info.title, "0001") self.assertEqual(len(info.tags), 2) - self.assertEqual(info.tags[0].slug, "tag1") - self.assertEqual(info.tags[1].slug, "tag2") + self.assertEqual(info.tags[0].name, "tag1") + self.assertEqual(info.tags[1].name, "tag2") self.assertIsNone(info.created) # Complex transformation with date in replacement string @@ -356,8 +356,8 @@ class TestFieldPermutations(TestCase): info = FileInfo.from_filename(filename) self.assertEqual(info.title, "0001") self.assertEqual(len(info.tags), 2) - self.assertEqual(info.tags[0].slug, "tag1") - self.assertEqual(info.tags[1].slug, "tag2") + self.assertEqual(info.tags[0].name, "tag1") + self.assertEqual(info.tags[1].name, "tag2") self.assertEqual(info.created.year, 2019) self.assertEqual(info.created.month, 9) self.assertEqual(info.created.day, 8) diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 08f7365da..a82c34f15 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -103,10 +103,7 @@ class MailAccountHandler(LoggingMixin): def _correspondent_from_name(self, name): try: - return Correspondent.objects.get_or_create( - name=name, defaults={ - "slug": slugify(name) - })[0] + return Correspondent.objects.get_or_create(name=name)[0] except DatabaseError as e: self.log( "error", From 33ca08f794f3fc6aad459b528e07f94692811b68 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 9 Dec 2020 00:07:22 +0100 Subject: [PATCH 04/37] tags from folders: case insensitive --- src/documents/management/commands/document_consumer.py | 4 +++- src/documents/tests/test_management_consumer.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index b2f689aed..8ac60aa6d 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -29,7 +29,9 @@ def _tags_from_path(filepath): path_parts = Path(filepath).relative_to( settings.CONSUMPTION_DIR).parent.parts for part in path_parts: - tag_ids.add(Tag.objects.get_or_create(name=part)[0].pk) + tag_ids.add(Tag.objects.get_or_create(name__iexact=part, defaults={ + "name": part + })[0].pk) return tag_ids diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index 6973fdacf..b6a61a167 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -230,7 +230,7 @@ class TestConsumerTags(DirectoriesMixin, ConsumerMixin, TransactionTestCase): tag_names = ("existingTag", "Space Tag") # Create a Tag prior to consuming a file using it in path - tag_ids = [Tag.objects.create(name=tag_names[0]).pk,] + tag_ids = [Tag.objects.create(name="existingtag").pk,] self.t_start() From 234915a23a999c60ddae50d27b4c122ea7ac314a Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 9 Dec 2020 13:27:02 +0100 Subject: [PATCH 05/37] fixed test case. fixed bug with the decryption logic. --- .../management/commands/decrypt_documents.py | 3 ++- src/documents/tests/test_management_archiver.py | 16 +++++++--------- src/documents/tests/test_management_decrypt.py | 10 +++++----- src/documents/tests/test_management_exporter.py | 12 +++++------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/documents/management/commands/decrypt_documents.py b/src/documents/management/commands/decrypt_documents.py index 2287bfa72..918f1a175 100644 --- a/src/documents/management/commands/decrypt_documents.py +++ b/src/documents/management/commands/decrypt_documents.py @@ -82,7 +82,8 @@ class Command(BaseCommand): with open(document.thumbnail_path, "wb") as f: f.write(raw_thumb) - document.save(update_fields=("storage_type", "filename")) + Document.objects.filter(id=document.id).update( + storage_type=document.storage_type, filename=document.filename) for path in old_paths: os.unlink(path) diff --git a/src/documents/tests/test_management_archiver.py b/src/documents/tests/test_management_archiver.py index fdb588acf..0828f05ff 100644 --- a/src/documents/tests/test_management_archiver.py +++ b/src/documents/tests/test_management_archiver.py @@ -16,25 +16,23 @@ sample_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") class TestArchiver(DirectoriesMixin, TestCase): def make_models(self): - self.d1 = Document.objects.create(checksum="A", title="A", content="first document", pk=1, mime_type="application/pdf") - #self.d2 = Document.objects.create(checksum="B", title="B", content="second document") - #self.d3 = Document.objects.create(checksum="C", title="C", content="unrelated document") + return Document.objects.create(checksum="A", title="A", content="first document", mime_type="application/pdf") def test_archiver(self): - shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, "0000001.pdf")) - self.make_models() + doc = self.make_models() + shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf")) call_command('document_archiver') def test_handle_document(self): - shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, "0000001.pdf")) - self.make_models() + doc = self.make_models() + shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf")) - handle_document(self.d1.pk) + handle_document(doc.pk) - doc = Document.objects.get(id=self.d1.id) + doc = Document.objects.get(id=doc.id) self.assertIsNotNone(doc.checksum) self.assertTrue(os.path.isfile(doc.archive_path)) diff --git a/src/documents/tests/test_management_decrypt.py b/src/documents/tests/test_management_decrypt.py index f68ea7cc1..1d64b1105 100644 --- a/src/documents/tests/test_management_decrypt.py +++ b/src/documents/tests/test_management_decrypt.py @@ -35,20 +35,20 @@ class TestDecryptDocuments(TestCase): PASSPHRASE="test" ).enable() - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000002.pdf.gpg"), os.path.join(originals_dir, "0000002.pdf.gpg")) - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000002.png.gpg"), os.path.join(thumb_dir, "0000002.png.gpg")) + doc = Document.objects.create(checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) - Document.objects.create(checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", id=2, mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000002.pdf.gpg"), os.path.join(originals_dir, "0000002.pdf.gpg")) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", f"0000002.png.gpg"), os.path.join(thumb_dir, f"{doc.id:07}.png.gpg")) call_command('decrypt_documents') - doc = Document.objects.get(id=2) + doc.refresh_from_db() self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) self.assertEqual(doc.filename, "0000002.pdf") self.assertTrue(os.path.isfile(os.path.join(originals_dir, "0000002.pdf"))) self.assertTrue(os.path.isfile(doc.source_path)) - self.assertTrue(os.path.isfile(os.path.join(thumb_dir, "0000002.png"))) + self.assertTrue(os.path.isfile(os.path.join(thumb_dir, f"{doc.id:07}.png"))) self.assertTrue(os.path.isfile(doc.thumbnail_path)) with doc.source_file as f: diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index ab9733dc4..22d6fc7f6 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -24,13 +24,14 @@ class TestExportImport(DirectoriesMixin, TestCase): file = os.path.join(self.dirs.originals_dir, "0000001.pdf") - Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow", filename="0000001.pdf", id=1, mime_type="application/pdf") - Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", id=2, mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) + Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow", filename="0000001.pdf", mime_type="application/pdf") + Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) Tag.objects.create(name="t") DocumentType.objects.create(name="dt") Correspondent.objects.create(name="c") target = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, target) call_command('document_exporter', target) @@ -66,9 +67,6 @@ class TestExportImport(DirectoriesMixin, TestCase): def test_export_missing_files(self): target = tempfile.mkdtemp() - Document.objects.create(checksum="AAAAAAAAAAAAAAAAA", title="wow", filename="0000004.pdf", id=3, mime_type="application/pdf") + self.addCleanup(shutil.rmtree, target) + Document.objects.create(checksum="AAAAAAAAAAAAAAAAA", title="wow", filename="0000004.pdf", mime_type="application/pdf") self.assertRaises(FileNotFoundError, call_command, 'document_exporter', target) - - def test_duplicate_titles(self): - # TODO - pass From 3677d0193f8dc2ad12e6e055d5c47ca3d54fd0ec Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 9 Dec 2020 13:44:37 +0100 Subject: [PATCH 06/37] shadows --- .../app/components/common/input/tags/tags.component.html | 2 +- .../widgets/widget-frame/widget-frame.component.html | 2 +- .../document-detail/document-detail.component.html | 2 +- .../components/document-list/document-list.component.html | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index b2ad0944f..8029dd860 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -8,7 +8,7 @@
-
+
diff --git a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html index d0f637935..1d7d2d906 100644 --- a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html +++ b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html @@ -1,4 +1,4 @@ -
+
{{title}}
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 9f4c72cdd..e0b5c6da9 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -17,7 +17,7 @@
- diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 8608ed92b..1a8c7a781 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -24,7 +24,7 @@
-
+
@@ -53,7 +53,7 @@
-