diff --git a/docs/changelog.rst b/docs/changelog.rst index 766d6424c..e995fd8c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog ######### +* 0.4.0 + * Introducing reminders. See `#199`_ for more information, but the short + explanation is that you can now attach simple notes & times to documents + which are made available via the API. Currently, the default API + (basically just the Django admin) doesn't really make use of this, but + `Thomas Brueggemann`_ over at `Paperless Desktop`_ has said that he would + like to make use of this feature in his project. + * 0.3.6 * Fix for `#200`_ (!!) where the API wasn't configured to allow updating the correspondent or the tags for a document. @@ -173,6 +181,8 @@ Changelog .. _Tim White: https://github.com/timwhite .. _Florian Harr: https://github.com/evils .. _Justin Snyman: https://github.com/stringlytyped +.. _Thomas Brueggemann: https://github.com/thomasbrueggemann +.. _Paperless Desktop: https://github.com/thomasbrueggemann/paperless-desktop .. _#20: https://github.com/danielquinn/paperless/issues/20 .. _#44: https://github.com/danielquinn/paperless/issues/44 @@ -199,4 +209,5 @@ Changelog .. _#171: https://github.com/danielquinn/paperless/issues/171 .. _#172: https://github.com/danielquinn/paperless/issues/172 .. _#179: https://github.com/danielquinn/paperless/pull/179 +.. _#199: https://github.com/danielquinn/paperless/issues/199 .. _#200: https://github.com/danielquinn/paperless/issues/200 diff --git a/src/documents/filters.py b/src/documents/filters.py index 9519ec37b..c3c60ccc7 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -8,7 +8,7 @@ class CorrespondentFilterSet(FilterSet): class Meta(object): model = Correspondent fields = { - 'name': [ + "name": [ "startswith", "endswith", "contains", "istartswith", "iendswith", "icontains" ], @@ -21,7 +21,7 @@ class TagFilterSet(FilterSet): class Meta(object): model = Tag fields = { - 'name': [ + "name": [ "startswith", "endswith", "contains", "istartswith", "iendswith", "icontains" ], diff --git a/src/documents/mixins.py b/src/documents/mixins.py index a031dd50f..4d4e9783f 100644 --- a/src/documents/mixins.py +++ b/src/documents/mixins.py @@ -1,8 +1,3 @@ -from django.contrib.auth.mixins import AccessMixin -from django.contrib.auth import authenticate, login -import base64 - - class Renderable(object): """ A handy mixin to make it easier/cleaner to print output based on a @@ -12,46 +7,3 @@ class Renderable(object): def _render(self, text, verbosity): if self.verbosity >= verbosity: print(text) - - -class SessionOrBasicAuthMixin(AccessMixin): - """ - Session or Basic Authentication mixin for Django. - It determines if the requester is already logged in or if they have - provided proper http-authorization and returning the view if all goes - well, otherwise responding with a 401. - - Base for mixin found here: https://djangosnippets.org/snippets/3073/ - """ - - def dispatch(self, request, *args, **kwargs): - - # check if user is authenticated via the session - if request.user.is_authenticated: - - # Already logged in, just return the view. - return super(SessionOrBasicAuthMixin, self).dispatch( - request, *args, **kwargs - ) - - # apparently not authenticated via session, maybe via HTTP Basic? - if 'HTTP_AUTHORIZATION' in request.META: - auth = request.META['HTTP_AUTHORIZATION'].split() - if len(auth) == 2: - # NOTE: Support for only basic authentication - if auth[0].lower() == "basic": - authString = base64.b64decode(auth[1]).decode('utf-8') - uname, passwd = authString.split(':') - user = authenticate(username=uname, password=passwd) - if user is not None: - if user.is_active: - login(request, user) - request.user = user - return super( - SessionOrBasicAuthMixin, self - ).dispatch( - request, *args, **kwargs - ) - - # nope, really not authenticated - return self.handle_no_permission() diff --git a/src/documents/static/paperless.css b/src/documents/static/paperless.css index 8b001bbcb..506debb6d 100644 --- a/src/documents/static/paperless.css +++ b/src/documents/static/paperless.css @@ -10,3 +10,14 @@ td a.tag { margin: 1px; display: inline-block; } + +#result_list th.column-note { + text-align: right; +} +#result_list td.field-note { + text-align: right; +} +#result_list td textarea { + width: 90%; + height: 5em; +} \ No newline at end of file diff --git a/src/documents/views.py b/src/documents/views.py index 3598472af..b57a0a571 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2,15 +2,16 @@ from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.generic import DetailView, FormView, TemplateView from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.filters import SearchFilter, OrderingFilter from paperless.db import GnuPG +from paperless.mixins import SessionOrBasicAuthMixin +from paperless.views import StandardPagination +from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.mixins import ( DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin ) -from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ( GenericViewSet, @@ -27,7 +28,6 @@ from .serialisers import ( LogSerializer, TagSerializer ) -from .mixins import SessionOrBasicAuthMixin class IndexView(TemplateView): @@ -92,12 +92,6 @@ class PushView(SessionOrBasicAuthMixin, FormView): return HttpResponse("0") -class StandardPagination(PageNumberPagination): - page_size = 25 - page_size_query_param = "page-size" - max_page_size = 100000 - - class CorrespondentViewSet(ModelViewSet): model = Correspondent queryset = Correspondent.objects.all() diff --git a/src/paperless/mixins.py b/src/paperless/mixins.py new file mode 100644 index 000000000..f4f1fcdec --- /dev/null +++ b/src/paperless/mixins.py @@ -0,0 +1,46 @@ +from django.contrib.auth.mixins import AccessMixin +from django.contrib.auth import authenticate, login +import base64 + + +class SessionOrBasicAuthMixin(AccessMixin): + """ + Session or Basic Authentication mixin for Django. + It determines if the requester is already logged in or if they have + provided proper http-authorization and returning the view if all goes + well, otherwise responding with a 401. + + Base for mixin found here: https://djangosnippets.org/snippets/3073/ + """ + + def dispatch(self, request, *args, **kwargs): + + # check if user is authenticated via the session + if request.user.is_authenticated: + + # Already logged in, just return the view. + return super(SessionOrBasicAuthMixin, self).dispatch( + request, *args, **kwargs + ) + + # apparently not authenticated via session, maybe via HTTP Basic? + if 'HTTP_AUTHORIZATION' in request.META: + auth = request.META['HTTP_AUTHORIZATION'].split() + if len(auth) == 2: + # NOTE: Support for only basic authentication + if auth[0].lower() == "basic": + authString = base64.b64decode(auth[1]).decode('utf-8') + uname, passwd = authString.split(':') + user = authenticate(username=uname, password=passwd) + if user is not None: + if user.is_active: + login(request, user) + request.user = user + return super( + SessionOrBasicAuthMixin, self + ).dispatch( + request, *args, **kwargs + ) + + # nope, really not authenticated + return self.handle_no_permission() diff --git a/src/paperless/settings.py b/src/paperless/settings.py index edd0da9f3..f95c69be5 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -61,6 +61,7 @@ INSTALLED_APPS = [ "django_extensions", "documents.apps.DocumentsConfig", + "reminders.apps.RemindersConfig", "paperless_tesseract.apps.PaperlessTesseractConfig", "flat_responsive", diff --git a/src/paperless/urls.py b/src/paperless/urls.py index a7775a588..b7bc13b10 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -24,12 +24,14 @@ from documents.views import ( IndexView, FetchView, PushView, CorrespondentViewSet, TagViewSet, DocumentViewSet, LogViewSet ) +from reminders.views import ReminderViewSet router = DefaultRouter() -router.register(r'correspondents', CorrespondentViewSet) -router.register(r'tags', TagViewSet) -router.register(r'documents', DocumentViewSet) -router.register(r'logs', LogViewSet) +router.register(r"correspondents", CorrespondentViewSet) +router.register(r"documents", DocumentViewSet) +router.register(r"logs", LogViewSet) +router.register(r"reminders", ReminderViewSet) +router.register(r"tags", TagViewSet) urlpatterns = [ diff --git a/src/paperless/views.py b/src/paperless/views.py new file mode 100644 index 000000000..d2ce23aaf --- /dev/null +++ b/src/paperless/views.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class StandardPagination(PageNumberPagination): + page_size = 25 + page_size_query_param = "page-size" + max_page_size = 100000 diff --git a/src/reminders/__init__.py b/src/reminders/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/reminders/admin.py b/src/reminders/admin.py new file mode 100644 index 000000000..bc39e24aa --- /dev/null +++ b/src/reminders/admin.py @@ -0,0 +1,20 @@ +from django.conf import settings +from django.contrib import admin + +from .models import Reminder + + +class ReminderAdmin(admin.ModelAdmin): + + class Media: + css = { + "all": ("paperless.css",) + } + + list_per_page = settings.PAPERLESS_LIST_PER_PAGE + list_display = ("date", "document", "note") + list_filter = ("date",) + list_editable = ("note",) + + +admin.site.register(Reminder, ReminderAdmin) diff --git a/src/reminders/apps.py b/src/reminders/apps.py new file mode 100644 index 000000000..a745cad29 --- /dev/null +++ b/src/reminders/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RemindersConfig(AppConfig): + name = "reminders" diff --git a/src/reminders/filters.py b/src/reminders/filters.py new file mode 100644 index 000000000..8ca8f4402 --- /dev/null +++ b/src/reminders/filters.py @@ -0,0 +1,14 @@ +from django_filters.rest_framework import CharFilter, FilterSet + +from .models import Reminder + + +class ReminderFilterSet(FilterSet): + + class Meta(object): + model = Reminder + fields = { + "document": ["exact"], + "date": ["gt", "lt", "gte", "lte", "exact"], + "note": ["istartswith", "iendswith", "icontains"] + } diff --git a/src/reminders/migrations/0001_initial.py b/src/reminders/migrations/0001_initial.py new file mode 100644 index 000000000..6daad4ef2 --- /dev/null +++ b/src/reminders/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-25 15:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('documents', '0016_auto_20170325_1558'), + ] + + operations = [ + migrations.CreateModel( + name='Reminder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField()), + ('note', models.TextField(blank=True)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.Document')), + ], + ), + ] diff --git a/src/reminders/migrations/__init__.py b/src/reminders/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/reminders/models.py b/src/reminders/models.py new file mode 100644 index 000000000..d6fb744f7 --- /dev/null +++ b/src/reminders/models.py @@ -0,0 +1,8 @@ +from django.db import models + + +class Reminder(models.Model): + + document = models.ForeignKey("documents.Document") + date = models.DateTimeField() + note = models.TextField(blank=True) diff --git a/src/reminders/serialisers.py b/src/reminders/serialisers.py new file mode 100644 index 000000000..bf8dd09c2 --- /dev/null +++ b/src/reminders/serialisers.py @@ -0,0 +1,14 @@ +from documents.models import Document +from rest_framework import serializers + +from .models import Reminder + + +class ReminderSerializer(serializers.HyperlinkedModelSerializer): + + document = serializers.HyperlinkedRelatedField( + view_name="drf:document-detail", queryset=Document.objects) + + class Meta(object): + model = Reminder + fields = ("id", "document", "date", "note") diff --git a/src/reminders/tests.py b/src/reminders/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/src/reminders/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/reminders/views.py b/src/reminders/views.py new file mode 100644 index 000000000..d2ead96b5 --- /dev/null +++ b/src/reminders/views.py @@ -0,0 +1,22 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import OrderingFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ( + ModelViewSet, +) + +from .filters import ReminderFilterSet +from .models import Reminder +from .serialisers import ReminderSerializer +from paperless.views import StandardPagination + + +class ReminderViewSet(ModelViewSet): + model = Reminder + queryset = Reminder.objects + serializer_class = ReminderSerializer + pagination_class = StandardPagination + permission_classes = (IsAuthenticated,) + filter_backends = (DjangoFilterBackend, OrderingFilter) + filter_class = ReminderFilterSet + ordering_fields = ("date", "document")