Merge branch 'dev' into feature-bulk-edit

This commit is contained in:
jonaswinkler
2020-12-15 03:13:22 +01:00
68 changed files with 1198 additions and 489 deletions

View File

@@ -4,7 +4,8 @@ from django.utils.safestring import mark_safe
from whoosh.writing import AsyncWriter
from . import index
from .models import Correspondent, Document, DocumentType, Log, Tag
from .models import Correspondent, Document, DocumentType, Log, Tag, \
SavedView, SavedViewFilterRule
class CorrespondentAdmin(admin.ModelAdmin):
@@ -131,8 +132,22 @@ class LogAdmin(admin.ModelAdmin):
list_display_links = ("created", "message")
class RuleInline(admin.TabularInline):
model = SavedViewFilterRule
class SavedViewAdmin(admin.ModelAdmin):
list_display = ("name", "user")
inlines = [
RuleInline
]
admin.site.register(Correspondent, CorrespondentAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(DocumentType, DocumentTypeAdmin)
admin.site.register(Document, DocumentAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(SavedView, SavedViewAdmin)

View File

@@ -8,6 +8,12 @@ from django.conf import settings
from django.template.defaultfilters import slugify
class defaultdictNoStr(defaultdict):
def __str__(self):
raise ValueError("Don't use {tags} directly.")
def create_source_path_directory(source_path):
os.makedirs(os.path.dirname(source_path), exist_ok=True)
@@ -90,8 +96,8 @@ def generate_filename(doc, counter=0):
try:
if settings.PAPERLESS_FILENAME_FORMAT is not None:
tags = defaultdict(lambda: slugify(None),
many_to_dictionary(doc.tags))
tags = defaultdictNoStr(lambda: slugify(None),
many_to_dictionary(doc.tags))
if doc.correspondent:
correspondent = pathvalidate.sanitize_filename(
@@ -114,14 +120,18 @@ def generate_filename(doc, counter=0):
document_type=document_type,
created=datetime.date.isoformat(doc.created),
created_year=doc.created.year if doc.created else "none",
created_month=doc.created.month if doc.created else "none",
created_day=doc.created.day if doc.created else "none",
created_month=f"{doc.created.month:02}" if doc.created else "none", # NOQA: E501
created_day=f"{doc.created.day:02}" if doc.created else "none",
added=datetime.date.isoformat(doc.added),
added_year=doc.added.year if doc.added else "none",
added_month=doc.added.month if doc.added else "none",
added_day=doc.added.day if doc.added else "none",
added_month=f"{doc.added.month:02}" if doc.added else "none",
added_day=f"{doc.added.day:02}" if doc.added else "none",
tags=tags,
)
tag_list=",".join([tag.name for tag in doc.tags.all()])
).strip()
path = path.strip(os.sep)
except (ValueError, KeyError, IndexError):
logging.getLogger(__name__).warning(
f"Invalid PAPERLESS_FILENAME_FORMAT: "

View File

@@ -0,0 +1,37 @@
# Generated by Django 3.1.4 on 2020-12-12 14:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('documents', '1006_auto_20201208_2209'),
]
operations = [
migrations.CreateModel(
name='SavedView',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('show_on_dashboard', models.BooleanField()),
('show_in_sidebar', models.BooleanField()),
('sort_field', models.CharField(max_length=128)),
('sort_reverse', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='SavedViewFilterRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rule_type', models.PositiveIntegerField(choices=[(0, 'Title contains'), (1, 'Content contains'), (2, 'ASN is'), (3, 'Correspondent is'), (4, 'Document type is'), (5, 'Is in inbox'), (6, 'Has tag'), (7, 'Has any tag'), (8, 'Created before'), (9, 'Created after'), (10, 'Created year is'), (11, 'Created month is'), (12, 'Created day is'), (13, 'Added before'), (14, 'Added after'), (15, 'Modified before'), (16, 'Modified after'), (17, 'Does not have tag')])),
('value', models.CharField(max_length=128)),
('saved_view', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filter_rules', to='documents.savedview')),
],
),
]

View File

@@ -9,6 +9,7 @@ import pathvalidate
import dateutil.parser
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
@@ -305,6 +306,47 @@ class Log(models.Model):
return self.message
class SavedView(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=128)
show_on_dashboard = models.BooleanField()
show_in_sidebar = models.BooleanField()
sort_field = models.CharField(max_length=128)
sort_reverse = models.BooleanField(default=False)
class SavedViewFilterRule(models.Model):
RULE_TYPES = [
(0, "Title contains"),
(1, "Content contains"),
(2, "ASN is"),
(3, "Correspondent is"),
(4, "Document type is"),
(5, "Is in inbox"),
(6, "Has tag"),
(7, "Has any tag"),
(8, "Created before"),
(9, "Created after"),
(10, "Created year is"),
(11, "Created month is"),
(12, "Created day is"),
(13, "Added before"),
(14, "Added after"),
(15, "Modified before"),
(16, "Modified after"),
(17, "Does not have tag"),
]
saved_view = models.ForeignKey(SavedView, on_delete=models.CASCADE, related_name="filter_rules")
rule_type = models.PositiveIntegerField(choices=RULE_TYPES)
value = models.CharField(max_length=128)
# TODO: why is this in the models file?
class FileInfo:

View File

@@ -4,7 +4,8 @@ from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
from . import bulk_edit
from .models import Correspondent, Tag, Document, Log, DocumentType
from .models import Correspondent, Tag, Document, Log, DocumentType, \
SavedView, SavedViewFilterRule
from .parsers import is_mime_type_supported
@@ -163,6 +164,43 @@ class LogSerializer(serializers.ModelSerializer):
)
class SavedViewFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = SavedViewFilterRule
fields = ["rule_type", "value"]
class SavedViewSerializer(serializers.ModelSerializer):
filter_rules = SavedViewFilterRuleSerializer(many=True)
class Meta:
model = SavedView
depth = 1
fields = ["id", "name", "show_on_dashboard", "show_in_sidebar",
"sort_field", "sort_reverse", "filter_rules"]
def update(self, instance, validated_data):
if 'filter_rules' in validated_data:
rules_data = validated_data.pop('filter_rules')
else:
rules_data = None
super(SavedViewSerializer, self).update(instance, validated_data)
if rules_data:
SavedViewFilterRule.objects.filter(saved_view=instance).delete()
for rule_data in rules_data:
SavedViewFilterRule.objects.create(saved_view=instance, **rule_data)
return instance
def create(self, validated_data):
rules_data = validated_data.pop('filter_rules')
saved_view = SavedView.objects.create(**validated_data)
for rule_data in rules_data:
SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data)
return saved_view
class BulkEditSerializer(serializers.Serializer):
documents = serializers.ListField(

View File

@@ -13,7 +13,7 @@ from django.test import TestCase, override_settings
from .utils import DirectoriesMixin
from ..file_handling import generate_filename, create_source_path_directory, delete_empty_directories, \
generate_unique_filename
from ..models import Document, Correspondent
from ..models import Document, Correspondent, Tag
class TestFileHandling(DirectoriesMixin, TestCase):
@@ -267,6 +267,57 @@ class TestFileHandling(DirectoriesMixin, TestCase):
self.assertEqual(generate_filename(document),
"none.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{tags}")
def test_tags_without_args(self):
document = Document()
document.mime_type = "application/pdf"
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
document.save()
self.assertEqual(generate_filename(document), f"{document.pk:07}.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{title} {tag_list}")
def test_tag_list(self):
doc = Document.objects.create(title="doc1", mime_type="application/pdf")
doc.tags.create(name="tag2")
doc.tags.create(name="tag1")
self.assertEqual(generate_filename(doc), "doc1 tag1,tag2.pdf")
doc = Document.objects.create(title="doc2", checksum="B", mime_type="application/pdf")
self.assertEqual(generate_filename(doc), "doc2.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="//etc/something/{title}")
def test_filename_relative(self):
doc = Document.objects.create(title="doc1", mime_type="application/pdf")
doc.filename = generate_filename(doc)
doc.save()
self.assertEqual(doc.source_path, os.path.join(settings.ORIGINALS_DIR, "etc", "something", "doc1.pdf"))
@override_settings(PAPERLESS_FILENAME_FORMAT="{created_year}-{created_month}-{created_day}")
def test_created_year_month_day(self):
d1 = datetime.datetime(2020, 3, 6, 1, 1, 1)
doc1 = Document.objects.create(title="doc1", mime_type="application/pdf", created=d1)
self.assertEqual(generate_filename(doc1), "2020-03-06.pdf")
doc1.created = datetime.datetime(2020, 11, 16, 1, 1, 1)
self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{added_year}-{added_month}-{added_day}")
def test_added_year_month_day(self):
d1 = datetime.datetime(232, 1, 9, 1, 1, 1)
doc1 = Document.objects.create(title="doc1", mime_type="application/pdf", added=d1)
self.assertEqual(generate_filename(doc1), "232-01-09.pdf")
doc1.added = datetime.datetime(2020, 11, 16, 1, 1, 1)
self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
@override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}")
def test_nested_directory_cleanup(self):
document = Document()

View File

@@ -38,7 +38,7 @@ from .filters import (
DocumentTypeFilterSet,
LogFilterSet
)
from .models import Correspondent, Document, Log, Tag, DocumentType
from .models import Correspondent, Document, Log, Tag, DocumentType, SavedView
from .parsers import get_parser_class_for_mime_type
from .serialisers import (
CorrespondentSerializer,
@@ -47,6 +47,7 @@ from .serialisers import (
TagSerializer,
DocumentTypeSerializer,
PostDocumentSerializer,
SavedViewSerializer,
BulkEditSerializer
)
@@ -256,6 +257,22 @@ class LogViewSet(ReadOnlyModelViewSet):
ordering_fields = ("created",)
class SavedViewViewSet(ModelViewSet):
model = SavedView
queryset = SavedView.objects.all()
serializer_class = SavedViewSerializer
pagination_class = StandardPagination
permission_classes = (IsAuthenticated,)
def get_queryset(self):
user = self.request.user
return SavedView.objects.filter(user=user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class BulkEditView(APIView):
permission_classes = (IsAuthenticated,)

View File

@@ -18,6 +18,7 @@ from documents.views import (
SearchAutoCompleteView,
StatisticsView,
PostDocumentView,
SavedViewViewSet,
BulkEditView
)
from paperless.views import FaviconView
@@ -28,6 +29,7 @@ api_router.register(r"document_types", DocumentTypeViewSet)
api_router.register(r"documents", DocumentViewSet)
api_router.register(r"logs", LogViewSet)
api_router.register(r"tags", TagViewSet)
api_router.register(r"saved_views", SavedViewViewSet)
urlpatterns = [