From dfa1f29809efc9a395e7e379ad6e2ba82983cc1f Mon Sep 17 00:00:00 2001
From: jonaswinkler <jonas.winkler@jpwinkler.de>
Date: Sat, 12 Dec 2020 15:46:56 +0100
Subject: [PATCH] add backend support for saved views

---
 src/documents/admin.py                        | 17 +++++++-
 .../1007_savedview_savedviewfilterrule.py     | 37 ++++++++++++++++
 src/documents/models.py                       | 42 +++++++++++++++++++
 src/documents/serialisers.py                  | 36 +++++++++++++++-
 src/documents/views.py                        | 21 +++++++++-
 src/paperless/urls.py                         |  4 +-
 6 files changed, 152 insertions(+), 5 deletions(-)
 create mode 100644 src/documents/migrations/1007_savedview_savedviewfilterrule.py

diff --git a/src/documents/admin.py b/src/documents/admin.py
index 055a6fd93..6ec3b736e 100755
--- a/src/documents/admin.py
+++ b/src/documents/admin.py
@@ -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)
diff --git a/src/documents/migrations/1007_savedview_savedviewfilterrule.py b/src/documents/migrations/1007_savedview_savedviewfilterrule.py
new file mode 100644
index 000000000..664def5f1
--- /dev/null
+++ b/src/documents/migrations/1007_savedview_savedviewfilterrule.py
@@ -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')),
+            ],
+        ),
+    ]
diff --git a/src/documents/models.py b/src/documents/models.py
index f0678a843..1b1f697bc 100755
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -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:
 
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index db0e610d1..43b5e5992 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -3,7 +3,8 @@ from django.utils.text import slugify
 from rest_framework import serializers
 from rest_framework.fields import SerializerMethodField
 
-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
 
 
@@ -140,6 +141,39 @@ 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):
+        rules_data = validated_data.pop('filter_rules')
+        super(SavedViewSerializer, self).update(instance, validated_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 PostDocumentSerializer(serializers.Serializer):
 
     document = serializers.FileField(
diff --git a/src/documents/views.py b/src/documents/views.py
index b42ae1f96..36d3445c4 100755
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -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,
@@ -46,7 +46,8 @@ from .serialisers import (
     LogSerializer,
     TagSerializer,
     DocumentTypeSerializer,
-    PostDocumentSerializer
+    PostDocumentSerializer,
+    SavedViewSerializer
 )
 
 
@@ -240,6 +241,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 PostDocumentView(APIView):
 
     permission_classes = (IsAuthenticated,)
diff --git a/src/paperless/urls.py b/src/paperless/urls.py
index 9b390b139..079971bb3 100755
--- a/src/paperless/urls.py
+++ b/src/paperless/urls.py
@@ -17,7 +17,8 @@ from documents.views import (
     IndexView,
     SearchAutoCompleteView,
     StatisticsView,
-    PostDocumentView
+    PostDocumentView,
+    SavedViewViewSet
 )
 from paperless.views import FaviconView
 
@@ -27,6 +28,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 = [