mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-22 02:51:17 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa4924d5ba | ||
|
|
5b88ebf0e7 | ||
|
|
a0edc7d54d | ||
|
|
b876a0d0df | ||
|
|
27db4f7e51 | ||
|
|
426919fa9f | ||
|
|
e47c152b81 |
@@ -1,6 +1,18 @@
|
|||||||
Changelog
|
Changelog
|
||||||
#########
|
#########
|
||||||
|
|
||||||
|
* 0.4.1
|
||||||
|
* Fix for `#206`_ wherein the pluggable parser didn't recognise files with
|
||||||
|
all-caps suffixes like ``.PDF``
|
||||||
|
|
||||||
|
* 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
|
* 0.3.6
|
||||||
* Fix for `#200`_ (!!) where the API wasn't configured to allow updating the
|
* Fix for `#200`_ (!!) where the API wasn't configured to allow updating the
|
||||||
correspondent or the tags for a document.
|
correspondent or the tags for a document.
|
||||||
@@ -173,6 +185,8 @@ Changelog
|
|||||||
.. _Tim White: https://github.com/timwhite
|
.. _Tim White: https://github.com/timwhite
|
||||||
.. _Florian Harr: https://github.com/evils
|
.. _Florian Harr: https://github.com/evils
|
||||||
.. _Justin Snyman: https://github.com/stringlytyped
|
.. _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
|
.. _#20: https://github.com/danielquinn/paperless/issues/20
|
||||||
.. _#44: https://github.com/danielquinn/paperless/issues/44
|
.. _#44: https://github.com/danielquinn/paperless/issues/44
|
||||||
@@ -199,4 +213,6 @@ Changelog
|
|||||||
.. _#171: https://github.com/danielquinn/paperless/issues/171
|
.. _#171: https://github.com/danielquinn/paperless/issues/171
|
||||||
.. _#172: https://github.com/danielquinn/paperless/issues/172
|
.. _#172: https://github.com/danielquinn/paperless/issues/172
|
||||||
.. _#179: https://github.com/danielquinn/paperless/pull/179
|
.. _#179: https://github.com/danielquinn/paperless/pull/179
|
||||||
|
.. _#199: https://github.com/danielquinn/paperless/issues/199
|
||||||
.. _#200: https://github.com/danielquinn/paperless/issues/200
|
.. _#200: https://github.com/danielquinn/paperless/issues/200
|
||||||
|
.. _#206: https://github.com/danielquinn/paperless/issues/206
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class Consumer(object):
|
|||||||
parser_class = self._get_parser_class(doc)
|
parser_class = self._get_parser_class(doc)
|
||||||
if not parser_class:
|
if not parser_class:
|
||||||
self.log(
|
self.log(
|
||||||
"info", "No parsers could be found for {}".format(doc))
|
"error", "No parsers could be found for {}".format(doc))
|
||||||
self._ignore.append(doc)
|
self._ignore.append(doc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -160,6 +160,16 @@ class Consumer(object):
|
|||||||
if result:
|
if result:
|
||||||
options.append(result)
|
options.append(result)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
"info",
|
||||||
|
"Parsers available: {}".format(
|
||||||
|
", ".join([str(o["parser"].__name__) for o in options])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not options:
|
||||||
|
return None
|
||||||
|
|
||||||
# Return the parser with the highest weight.
|
# Return the parser with the highest weight.
|
||||||
return sorted(
|
return sorted(
|
||||||
options, key=lambda _: _["weight"], reverse=True)[0]["parser"]
|
options, key=lambda _: _["weight"], reverse=True)[0]["parser"]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class CorrespondentFilterSet(FilterSet):
|
|||||||
class Meta(object):
|
class Meta(object):
|
||||||
model = Correspondent
|
model = Correspondent
|
||||||
fields = {
|
fields = {
|
||||||
'name': [
|
"name": [
|
||||||
"startswith", "endswith", "contains",
|
"startswith", "endswith", "contains",
|
||||||
"istartswith", "iendswith", "icontains"
|
"istartswith", "iendswith", "icontains"
|
||||||
],
|
],
|
||||||
@@ -21,7 +21,7 @@ class TagFilterSet(FilterSet):
|
|||||||
class Meta(object):
|
class Meta(object):
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = {
|
fields = {
|
||||||
'name': [
|
"name": [
|
||||||
"startswith", "endswith", "contains",
|
"startswith", "endswith", "contains",
|
||||||
"istartswith", "iendswith", "icontains"
|
"istartswith", "iendswith", "icontains"
|
||||||
],
|
],
|
||||||
|
|||||||
20
src/documents/migrations/0016_auto_20170325_1558.py
Normal file
20
src/documents/migrations/0016_auto_20170325_1558.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '0015_add_insensitive_to_match'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, db_index=True, help_text='The raw, text-only data of the document. This field is primarily used for searching.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
from django.contrib.auth.mixins import AccessMixin
|
|
||||||
from django.contrib.auth import authenticate, login
|
|
||||||
import base64
|
|
||||||
|
|
||||||
|
|
||||||
class Renderable(object):
|
class Renderable(object):
|
||||||
"""
|
"""
|
||||||
A handy mixin to make it easier/cleaner to print output based on a
|
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):
|
def _render(self, text, verbosity):
|
||||||
if self.verbosity >= verbosity:
|
if self.verbosity >= verbosity:
|
||||||
print(text)
|
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()
|
|
||||||
|
|||||||
@@ -10,3 +10,14 @@ td a.tag {
|
|||||||
margin: 1px;
|
margin: 1px;
|
||||||
display: inline-block;
|
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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,56 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from ..consumer import Consumer
|
||||||
from ..models import FileInfo
|
from ..models import FileInfo
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsumer(TestCase):
|
||||||
|
|
||||||
|
class DummyParser(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test__get_parser_class_1_parser(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self._get_consumer()._get_parser_class("doc.pdf"),
|
||||||
|
self.DummyParser
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.os.makedirs")
|
||||||
|
@mock.patch("documents.consumer.os.path.exists", return_value=True)
|
||||||
|
@mock.patch("documents.consumer.document_consumer_declaration.send")
|
||||||
|
def test__get_parser_class_n_parsers(self, m, *args):
|
||||||
|
|
||||||
|
class DummyParser1(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DummyParser2(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
m.return_value = (
|
||||||
|
(None, lambda _: {"weight": 0, "parser": DummyParser1}),
|
||||||
|
(None, lambda _: {"weight": 1, "parser": DummyParser2}),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(Consumer()._get_parser_class("doc.pdf"), DummyParser2)
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.os.makedirs")
|
||||||
|
@mock.patch("documents.consumer.os.path.exists", return_value=True)
|
||||||
|
@mock.patch("documents.consumer.document_consumer_declaration.send")
|
||||||
|
def test__get_parser_class_0_parsers(self, m, *args):
|
||||||
|
m.return_value = ((None, lambda _: None),)
|
||||||
|
self.assertIsNone(Consumer()._get_parser_class("doc.pdf"))
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.os.makedirs")
|
||||||
|
@mock.patch("documents.consumer.os.path.exists", return_value=True)
|
||||||
|
@mock.patch("documents.consumer.document_consumer_declaration.send")
|
||||||
|
def _get_consumer(self, m, *args):
|
||||||
|
m.return_value = (
|
||||||
|
(None, lambda _: {"weight": 0, "parser": self.DummyParser}),
|
||||||
|
)
|
||||||
|
return Consumer()
|
||||||
|
|
||||||
|
|
||||||
class TestAttributes(TestCase):
|
class TestAttributes(TestCase):
|
||||||
|
|
||||||
TAGS = ("tag1", "tag2", "tag3")
|
TAGS = ("tag1", "tag2", "tag3")
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ from django.http import HttpResponse
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import DetailView, FormView, TemplateView
|
from django.views.generic import DetailView, FormView, TemplateView
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
|
||||||
from paperless.db import GnuPG
|
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 (
|
from rest_framework.mixins import (
|
||||||
DestroyModelMixin,
|
DestroyModelMixin,
|
||||||
ListModelMixin,
|
ListModelMixin,
|
||||||
RetrieveModelMixin,
|
RetrieveModelMixin,
|
||||||
UpdateModelMixin
|
UpdateModelMixin
|
||||||
)
|
)
|
||||||
from rest_framework.pagination import PageNumberPagination
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.viewsets import (
|
from rest_framework.viewsets import (
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
@@ -27,7 +28,6 @@ from .serialisers import (
|
|||||||
LogSerializer,
|
LogSerializer,
|
||||||
TagSerializer
|
TagSerializer
|
||||||
)
|
)
|
||||||
from .mixins import SessionOrBasicAuthMixin
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
@@ -92,12 +92,6 @@ class PushView(SessionOrBasicAuthMixin, FormView):
|
|||||||
return HttpResponse("0")
|
return HttpResponse("0")
|
||||||
|
|
||||||
|
|
||||||
class StandardPagination(PageNumberPagination):
|
|
||||||
page_size = 25
|
|
||||||
page_size_query_param = "page-size"
|
|
||||||
max_page_size = 100000
|
|
||||||
|
|
||||||
|
|
||||||
class CorrespondentViewSet(ModelViewSet):
|
class CorrespondentViewSet(ModelViewSet):
|
||||||
model = Correspondent
|
model = Correspondent
|
||||||
queryset = Correspondent.objects.all()
|
queryset = Correspondent.objects.all()
|
||||||
|
|||||||
46
src/paperless/mixins.py
Normal file
46
src/paperless/mixins.py
Normal file
@@ -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()
|
||||||
@@ -61,6 +61,7 @@ INSTALLED_APPS = [
|
|||||||
"django_extensions",
|
"django_extensions",
|
||||||
|
|
||||||
"documents.apps.DocumentsConfig",
|
"documents.apps.DocumentsConfig",
|
||||||
|
"reminders.apps.RemindersConfig",
|
||||||
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
||||||
|
|
||||||
"flat_responsive",
|
"flat_responsive",
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ from documents.views import (
|
|||||||
IndexView, FetchView, PushView,
|
IndexView, FetchView, PushView,
|
||||||
CorrespondentViewSet, TagViewSet, DocumentViewSet, LogViewSet
|
CorrespondentViewSet, TagViewSet, DocumentViewSet, LogViewSet
|
||||||
)
|
)
|
||||||
|
from reminders.views import ReminderViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'correspondents', CorrespondentViewSet)
|
router.register(r"correspondents", CorrespondentViewSet)
|
||||||
router.register(r'tags', TagViewSet)
|
router.register(r"documents", DocumentViewSet)
|
||||||
router.register(r'documents', DocumentViewSet)
|
router.register(r"logs", LogViewSet)
|
||||||
router.register(r'logs', LogViewSet)
|
router.register(r"reminders", ReminderViewSet)
|
||||||
|
router.register(r"tags", TagViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
|||||||
7
src/paperless/views.py
Normal file
7
src/paperless/views.py
Normal file
@@ -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
|
||||||
@@ -5,7 +5,7 @@ from .parsers import RasterisedDocumentParser
|
|||||||
|
|
||||||
class ConsumerDeclaration(object):
|
class ConsumerDeclaration(object):
|
||||||
|
|
||||||
MATCHING_FILES = re.compile("^.*\.(pdf|jpg|gif|png|tiff|pnm|bmp)$")
|
MATCHING_FILES = re.compile("^.*\.(pdf|jpg|gif|png|tiff?|pnm|bmp)$")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle(cls, sender, **kwargs):
|
def handle(cls, sender, **kwargs):
|
||||||
@@ -14,7 +14,7 @@ class ConsumerDeclaration(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def test(cls, doc):
|
def test(cls, doc):
|
||||||
|
|
||||||
if cls.MATCHING_FILES.match(doc):
|
if cls.MATCHING_FILES.match(doc.lower()):
|
||||||
return {
|
return {
|
||||||
"parser": RasterisedDocumentParser,
|
"parser": RasterisedDocumentParser,
|
||||||
"weight": 0
|
"weight": 0
|
||||||
|
|||||||
36
src/paperless_tesseract/tests/test_signals.py
Normal file
36
src/paperless_tesseract/tests/test_signals.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from ..signals import ConsumerDeclaration
|
||||||
|
|
||||||
|
|
||||||
|
class SignalsTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_test_handles_various_file_names_true(self):
|
||||||
|
|
||||||
|
prefixes = (
|
||||||
|
"doc", "My Document", "Μυ Γρεεκ Δοψθμεντ", "Doc -with - tags",
|
||||||
|
"A document with a . in it", "Doc with -- in it"
|
||||||
|
)
|
||||||
|
suffixes = (
|
||||||
|
"pdf", "jpg", "gif", "png", "tiff", "tif", "pnm", "bmp",
|
||||||
|
"PDF", "JPG", "GIF", "PNG", "TIFF", "TIF", "PNM", "BMP",
|
||||||
|
"pDf", "jPg", "gIf", "pNg", "tIff", "tIf", "pNm", "bMp",
|
||||||
|
)
|
||||||
|
|
||||||
|
for prefix in prefixes:
|
||||||
|
for suffix in suffixes:
|
||||||
|
name = "{}.{}".format(prefix, suffix)
|
||||||
|
self.assertTrue(ConsumerDeclaration.test(name))
|
||||||
|
|
||||||
|
def test_test_handles_various_file_names_false(self):
|
||||||
|
|
||||||
|
prefixes = ("doc",)
|
||||||
|
suffixes = ("txt", "markdown", "",)
|
||||||
|
|
||||||
|
for prefix in prefixes:
|
||||||
|
for suffix in suffixes:
|
||||||
|
name = "{}.{}".format(prefix, suffix)
|
||||||
|
self.assertFalse(ConsumerDeclaration.test(name))
|
||||||
|
|
||||||
|
self.assertFalse(ConsumerDeclaration.test(""))
|
||||||
|
self.assertFalse(ConsumerDeclaration.test("doc"))
|
||||||
0
src/reminders/__init__.py
Normal file
0
src/reminders/__init__.py
Normal file
20
src/reminders/admin.py
Normal file
20
src/reminders/admin.py
Normal file
@@ -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)
|
||||||
5
src/reminders/apps.py
Normal file
5
src/reminders/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RemindersConfig(AppConfig):
|
||||||
|
name = "reminders"
|
||||||
14
src/reminders/filters.py
Normal file
14
src/reminders/filters.py
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
27
src/reminders/migrations/0001_initial.py
Normal file
27
src/reminders/migrations/0001_initial.py
Normal file
@@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/reminders/migrations/__init__.py
Normal file
0
src/reminders/migrations/__init__.py
Normal file
8
src/reminders/models.py
Normal file
8
src/reminders/models.py
Normal file
@@ -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)
|
||||||
14
src/reminders/serialisers.py
Normal file
14
src/reminders/serialisers.py
Normal file
@@ -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")
|
||||||
3
src/reminders/tests.py
Normal file
3
src/reminders/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
22
src/reminders/views.py
Normal file
22
src/reminders/views.py
Normal file
@@ -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")
|
||||||
Reference in New Issue
Block a user