mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-12 00:19:48 +00:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3ca215e4dc | ||
![]() |
16c4183333 | ||
![]() |
6fe37678f2 | ||
![]() |
b58188f805 | ||
![]() |
f2a42ab6fe | ||
![]() |
e236b7bf7b | ||
![]() |
35004f434b | ||
![]() |
75251ad694 | ||
![]() |
870357968a | ||
![]() |
a593798b4b | ||
![]() |
4f070ba162 | ||
![]() |
9517d27f40 | ||
![]() |
35bb3dbcc2 | ||
![]() |
06117929bb | ||
![]() |
d1c8241947 | ||
![]() |
4c38b28469 | ||
![]() |
ad0f0a0b5d | ||
![]() |
83746a9aeb | ||
![]() |
6a36a4ec97 | ||
![]() |
af4623e605 | ||
![]() |
db8e116681 | ||
![]() |
a8616ebfe2 | ||
![]() |
a38d3bf7f8 | ||
![]() |
1cb5bbd07d | ||
![]() |
6edb5b912f | ||
![]() |
ec20c7577e | ||
![]() |
d6df9b3656 | ||
![]() |
80a849fef7 | ||
![]() |
bd67b53d50 | ||
![]() |
e32ed09da3 | ||
![]() |
c5632e5c04 | ||
![]() |
4d2b71454d | ||
![]() |
5cbb33b02b | ||
![]() |
2c55aad6c0 | ||
![]() |
1e039dcb32 | ||
![]() |
6ca8da4858 | ||
![]() |
82f05e27c3 | ||
![]() |
7a627e4ad8 | ||
![]() |
73af9552ec | ||
![]() |
e4854f2144 | ||
![]() |
6f5c1ac4e1 | ||
![]() |
22acc51284 | ||
![]() |
a05644fc31 | ||
![]() |
d1aa54caa9 | ||
![]() |
e293f70a91 | ||
![]() |
347986a2b3 | ||
![]() |
ede274386b | ||
![]() |
3e083354cc | ||
![]() |
b2b4f6516a | ||
![]() |
2ae702c7bb | ||
![]() |
b748420a94 | ||
![]() |
8a4546ce0d | ||
![]() |
167412a003 | ||
![]() |
e8d90b42a1 | ||
![]() |
d8c7e9de5f | ||
![]() |
2ac1b78a2c | ||
![]() |
e8e38befb7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -68,6 +68,7 @@ db.sqlite3
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Other stuff that doesn't belong
|
# Other stuff that doesn't belong
|
||||||
|
.virtualenv
|
||||||
virtualenv
|
virtualenv
|
||||||
.vagrant
|
.vagrant
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
@@ -26,7 +26,8 @@ How it Works
|
|||||||
Paperless does not control your scanner, it only helps you deal with what your
|
Paperless does not control your scanner, it only helps you deal with what your
|
||||||
scanner produces
|
scanner produces
|
||||||
|
|
||||||
1. Buy a document scanner like `this one`_ (used by me) or `this other one`_
|
1. Buy a document scanner that can write to a place on your network. If you
|
||||||
|
need some inspiration, have a look at the `scanner recommendations`_ page.
|
||||||
recommended by another user.
|
recommended by another user.
|
||||||
2. Set it up to "scan to FTP" or something similar. It should be able to push
|
2. Set it up to "scan to FTP" or something similar. It should be able to push
|
||||||
scanned images to a server without you having to do anything. If your
|
scanned images to a server without you having to do anything. If your
|
||||||
@@ -118,8 +119,7 @@ The thing is, I'm doing ok for money, so I would instead ask you to donate to
|
|||||||
the `United Nations High Commissioner for Refugees`_. They're doing important
|
the `United Nations High Commissioner for Refugees`_. They're doing important
|
||||||
work and they need the money a lot more than I do.
|
work and they need the money a lot more than I do.
|
||||||
|
|
||||||
.. _this one: http://www.brother.ca/en-CA/Scanners/11/ProductDetail/ADS1500W?ProductDetail=productdetail
|
.. _scanner recommendations: https://paperless.readthedocs.io/en/latest/scanners.html
|
||||||
.. _this other one: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
|
|
||||||
.. _ImageMagick: http://imagemagick.org/
|
.. _ImageMagick: http://imagemagick.org/
|
||||||
.. _Tesseract: https://github.com/tesseract-ocr
|
.. _Tesseract: https://github.com/tesseract-ocr
|
||||||
.. _Unpaper: https://www.flameeyes.eu/projects/unpaper
|
.. _Unpaper: https://www.flameeyes.eu/projects/unpaper
|
||||||
@@ -140,5 +140,5 @@ work and they need the money a lot more than I do.
|
|||||||
:target: https://gitter.im/danielquinn/paperless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
:target: https://gitter.im/danielquinn/paperless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||||
.. |Travis| image:: https://travis-ci.org/danielquinn/paperless.svg?branch=master
|
.. |Travis| image:: https://travis-ci.org/danielquinn/paperless.svg?branch=master
|
||||||
:target: https://travis-ci.org/danielquinn/paperless
|
:target: https://travis-ci.org/danielquinn/paperless
|
||||||
.. |Dependencies| image:: https://www.versioneye.com/user/projects/57b33b81d9f1b00016faa500/badge.svg?style=flat-square
|
.. |Dependencies| image:: https://www.versioneye.com/user/projects/57b33b81d9f1b00016faa500/badge.svg
|
||||||
:target: https://www.versioneye.com/user/projects/57b33b81d9f1b00016faa500
|
:target: https://www.versioneye.com/user/projects/57b33b81d9f1b00016faa500
|
||||||
|
5
Vagrantfile
vendored
5
Vagrantfile
vendored
@@ -12,4 +12,9 @@ Vagrant.configure(VAGRANT_API_VERSION) do |config|
|
|||||||
|
|
||||||
# Networking details
|
# Networking details
|
||||||
config.vm.network "private_network", ip: "172.28.128.4"
|
config.vm.network "private_network", ip: "172.28.128.4"
|
||||||
|
|
||||||
|
config.vm.provider "virtualbox" do |vb|
|
||||||
|
# Customize the amount of memory on the VM:
|
||||||
|
vb.memory = "1024"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@@ -17,7 +17,7 @@ services:
|
|||||||
# value with nothing.
|
# value with nothing.
|
||||||
environment:
|
environment:
|
||||||
- PAPERLESS_OCR_LANGUAGES=
|
- PAPERLESS_OCR_LANGUAGES=
|
||||||
command: ["runserver", "0.0.0.0:8000"]
|
command: ["runserver", "--insecure", "0.0.0.0:8000"]
|
||||||
|
|
||||||
consumer:
|
consumer:
|
||||||
image: pitkley/paperless
|
image: pitkley/paperless
|
||||||
@@ -26,7 +26,7 @@ services:
|
|||||||
- media:/usr/src/paperless/media
|
- media:/usr/src/paperless/media
|
||||||
# You have to adapt the local path you want the consumption
|
# You have to adapt the local path you want the consumption
|
||||||
# directory to mount to by modifying the part before the ':'.
|
# directory to mount to by modifying the part before the ':'.
|
||||||
- /path/to/arbitrary/place:/consume
|
- ./consume:/consume
|
||||||
# Likewise, you can add a local path to mount a directory for
|
# Likewise, you can add a local path to mount a directory for
|
||||||
# exporting. This is not strictly needed for paperless to
|
# exporting. This is not strictly needed for paperless to
|
||||||
# function, only if you're exporting your files: uncomment
|
# function, only if you're exporting your files: uncomment
|
||||||
|
@@ -1,6 +1,38 @@
|
|||||||
Changelog
|
Changelog
|
||||||
#########
|
#########
|
||||||
|
|
||||||
|
* 1.0.0
|
||||||
|
* Upgrade to Django 1.11. **You'll need to run
|
||||||
|
``pip install -r requirements.txt`` to after the usual ``git pull`` to
|
||||||
|
properly update**.
|
||||||
|
* Replace the templatetag-based hack we had for document listing in favour of
|
||||||
|
a slightly less ugly solution in the form of another template tag with less
|
||||||
|
copypasta.
|
||||||
|
* Support for multi-word-matches for auto-tagging thanks to an excellent
|
||||||
|
patch from `ishirav`_ `#277`_.
|
||||||
|
* Fixed a CSS bug reported by `Stefan Hagen`_ that caused an overlapping of
|
||||||
|
the text and checkboxes under some resolutions `#272`_.
|
||||||
|
* Patched the Docker config to force the serving of static files. Credit for
|
||||||
|
this one goes to `dev-rke`_ via `#248`_.
|
||||||
|
* Fix file permissions during Docker start up thanks to `Pit`_ on `#268`_.
|
||||||
|
* Date fields in the admin are now expressed as HTML5 date fields thanks to
|
||||||
|
`Lukas Winkler`_'s issue `#278`_
|
||||||
|
|
||||||
|
* 0.8.0
|
||||||
|
* Paperless can now run in a subdirectory on a host (``/paperless``), rather
|
||||||
|
than always running in the root (``/``) thanks to `maphy-psd`_'s work on
|
||||||
|
`#255`_.
|
||||||
|
|
||||||
|
* 0.7.0
|
||||||
|
* **Potentially breaking change**: As per `#235`_, Paperless will no longer
|
||||||
|
automatically delete documents attached to correspondents when those
|
||||||
|
correspondents are themselves deleted. This was Django's default
|
||||||
|
behaviour, but didn't make much sense in Paperless' case. Thanks to
|
||||||
|
`Thomas Brueggemann`_ and `David Martin`_ for their input on this one.
|
||||||
|
* Fix for `#232`_ wherein Paperless wasn't recognising ``.tif`` files
|
||||||
|
properly. Thanks to `ayounggun`_ for reporting this one and to
|
||||||
|
`Kusti Skytén`_ for posting the correct solution in the Github issue.
|
||||||
|
|
||||||
* 0.6.0
|
* 0.6.0
|
||||||
* Abandon the shared-secret trick we were using for the POST API in favour
|
* Abandon the shared-secret trick we were using for the POST API in favour
|
||||||
of BasicAuth or Django session.
|
of BasicAuth or Django session.
|
||||||
@@ -219,6 +251,13 @@ Changelog
|
|||||||
.. _David Martin: https://github.com/ddddavidmartin
|
.. _David Martin: https://github.com/ddddavidmartin
|
||||||
.. _Paperless Desktop: https://github.com/thomasbrueggemann/paperless-desktop
|
.. _Paperless Desktop: https://github.com/thomasbrueggemann/paperless-desktop
|
||||||
.. _Joshua Gilman: https://github.com/jmgilman
|
.. _Joshua Gilman: https://github.com/jmgilman
|
||||||
|
.. _ayounggun: https://github.com/ayounggun
|
||||||
|
.. _Kusti Skytén: https://github.com/kskyten
|
||||||
|
.. _maphy-psd: https://github.com/maphy-psd
|
||||||
|
.. _ishirav: https://github.com/ishirav
|
||||||
|
.. _Stefan Hagen: https://github.com/xkpd3
|
||||||
|
.. _dev-rke: https://github.com/dev-rke
|
||||||
|
.. _Lukas Winkler: https://github.com/Findus23
|
||||||
|
|
||||||
.. _#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
|
||||||
@@ -256,5 +295,12 @@ Changelog
|
|||||||
.. _#228: https://github.com/danielquinn/paperless/pull/228
|
.. _#228: https://github.com/danielquinn/paperless/pull/228
|
||||||
.. _#229: https://github.com/danielquinn/paperless/pull/229
|
.. _#229: https://github.com/danielquinn/paperless/pull/229
|
||||||
.. _#230: https://github.com/danielquinn/paperless/pull/230
|
.. _#230: https://github.com/danielquinn/paperless/pull/230
|
||||||
|
.. _#232: https://github.com/danielquinn/paperless/issues/232
|
||||||
|
.. _#235: https://github.com/danielquinn/paperless/issues/235
|
||||||
.. _#236: https://github.com/danielquinn/paperless/issues/236
|
.. _#236: https://github.com/danielquinn/paperless/issues/236
|
||||||
|
.. _#255: https://github.com/danielquinn/paperless/pull/255
|
||||||
|
.. _#268: https://github.com/danielquinn/paperless/pull/268
|
||||||
|
.. _#277: https://github.com/danielquinn/paperless/pull/277
|
||||||
|
.. _#272: https://github.com/danielquinn/paperless/issues/272
|
||||||
|
.. _#248: https://github.com/danielquinn/paperless/issues/248
|
||||||
|
.. _#278: https://github.com/danielquinn/paperless/issues/248
|
||||||
|
@@ -80,6 +80,12 @@ text and matching algorithm. From the help info there:
|
|||||||
uses a regex to match the PDF. If you don't know what a regex is, you
|
uses a regex to match the PDF. If you don't know what a regex is, you
|
||||||
probably don't want this option.
|
probably don't want this option.
|
||||||
|
|
||||||
|
When using the "any" or "all" matching algorithms, you can search for terms that
|
||||||
|
consist of multiple words by enclosing them in double quotes. For example, defining
|
||||||
|
a match text of ``"Bank of America" BofA`` using the "any" algorithm, will match
|
||||||
|
documents that contain either "Bank of America" or "BofA", but will not match
|
||||||
|
documents containing "Bank of South America".
|
||||||
|
|
||||||
Then just save your tag/correspondent and run another document through the
|
Then just save your tag/correspondent and run another document through the
|
||||||
consumer. Once complete, you should see the newly-created document,
|
consumer. Once complete, you should see the newly-created document,
|
||||||
automatically tagged with the appropriate data.
|
automatically tagged with the appropriate data.
|
||||||
|
@@ -40,4 +40,5 @@ Contents
|
|||||||
guesswork
|
guesswork
|
||||||
migrating
|
migrating
|
||||||
troubleshooting
|
troubleshooting
|
||||||
|
scanners
|
||||||
changelog
|
changelog
|
||||||
|
29
docs/scanners.rst
Normal file
29
docs/scanners.rst
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.. _scanners:
|
||||||
|
|
||||||
|
Scanner Recommendations
|
||||||
|
=======================
|
||||||
|
|
||||||
|
As Paperless operates by watching a folder for new files, doesn't care what
|
||||||
|
scanner you use, but sometimes finding a scanner that will write to an FTP,
|
||||||
|
NFS, or SMB server can be difficult. This page is here to help you find one
|
||||||
|
that works right for you based on recommentations from other Paperless users.
|
||||||
|
|
||||||
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
|
| Brand | Model | Supports | Recommended By |
|
||||||
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
|
| | | FTP | NFS | SMB | |
|
||||||
|
+=========+================+=====+=====+=====+================+
|
||||||
|
| Brother | `ADS-1500W`_ | yes | no | yes | `danielquinn`_ |
|
||||||
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
|
| Brother | `MFC-J6930DW`_ | yes | | | `ayounggun`_ |
|
||||||
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
|
| Fujitsu | `ix500`_ | yes | | yes | `eonist`_ |
|
||||||
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
|
|
||||||
|
.. _ADS-1500W: https://www.brother.ca/en/p/ads1500w
|
||||||
|
.. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
|
||||||
|
.. _ix500: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
|
||||||
|
|
||||||
|
.. _danielquinn: https://github.com/danielquinn
|
||||||
|
.. _ayounggun: https://github.com/ayounggun
|
||||||
|
.. _eonist: https://github.com/eonist
|
@@ -394,7 +394,10 @@ Using a Real Webserver
|
|||||||
The default is to use Django's development server, as that's easy and does the
|
The default is to use Django's development server, as that's easy and does the
|
||||||
job well enough on a home network. However, if you want to do things right,
|
job well enough on a home network. However, if you want to do things right,
|
||||||
it's probably a good idea to use a webserver capable of handling more than one
|
it's probably a good idea to use a webserver capable of handling more than one
|
||||||
thread.
|
thread. You will also have to let the webserver serve the static files (CSS,
|
||||||
|
JavaScript) from the directory configured in ``PAPERLESS_STATICDIR``. For that,
|
||||||
|
you need to run ``./manage.py collectstatic`` in the ``src`` directory. The
|
||||||
|
default static files directory is ``../static``.
|
||||||
|
|
||||||
Apache
|
Apache
|
||||||
~~~~~~
|
~~~~~~
|
||||||
@@ -572,3 +575,28 @@ If you're using Docker, you can set a restart-policy_ in the
|
|||||||
Docker daemon.
|
Docker daemon.
|
||||||
|
|
||||||
.. _restart-policy: https://docs.docker.com/engine/reference/commandline/run/#restart-policies-restart
|
.. _restart-policy: https://docs.docker.com/engine/reference/commandline/run/#restart-policies-restart
|
||||||
|
|
||||||
|
|
||||||
|
.. _setup-subdirectory
|
||||||
|
|
||||||
|
Hosting Paperless in a Subdirectory
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
Paperless was designed to run off the root of the hosting domain,
|
||||||
|
(ie: ``https://example.com/``) but with a few changes, you can configure
|
||||||
|
it to run in a subdirectory on your server
|
||||||
|
(ie: ``https://example.com/paperless/``).
|
||||||
|
|
||||||
|
Thanks to the efforts of `maphy-psd`_ on `Github`_, running Paperless in a
|
||||||
|
subdirectory is now as easy as setting a config variable. Simply set
|
||||||
|
``PAPERLESS_FORCE_SCRIPT_NAME`` in your environment or
|
||||||
|
``/etc/paperless.conf`` to the path you want Paperless hosted at, configure
|
||||||
|
Nginx/Apache for your needs and you're done. So, if you want Paperless to live
|
||||||
|
at ``https://example.com/arbitrary/path/to/paperless`` then you just set
|
||||||
|
``PAPERLESS_FORCE_SCRIPT_NAME`` to ``/arbitrary/path/to/paperless``. Note the
|
||||||
|
leading ``/`` there.
|
||||||
|
|
||||||
|
As to how to configure Nginx or Apache for this, that's on you :-)
|
||||||
|
|
||||||
|
.. _maphy-psd: https://github.com/maphy-psd
|
||||||
|
.. _Github: https://github.com/danielquinn/paperless/pull/255
|
||||||
|
@@ -80,6 +80,11 @@ PAPERLESS_PASSPHRASE="secret"
|
|||||||
# as is "example.com,www.example.com", but NOT " example.com" or "example.com,"
|
# as is "example.com,www.example.com", but NOT " example.com" or "example.com,"
|
||||||
#PAPERLESS_ALLOWED_HOSTS="example.com,www.example.com"
|
#PAPERLESS_ALLOWED_HOSTS="example.com,www.example.com"
|
||||||
|
|
||||||
|
# To host paperless under a subpath url like example.com/paperless you set
|
||||||
|
# this value to /paperless. No trailing slash!
|
||||||
|
#
|
||||||
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#force-script-name
|
||||||
|
#PAPERLESS_FORCE_SCRIPT_NAME=""
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
#### Software Tweaks ####
|
#### Software Tweaks ####
|
||||||
@@ -156,7 +161,9 @@ PAPERLESS_PASSPHRASE="secret"
|
|||||||
#### Interface ####
|
#### Interface ####
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Override the default UTC time zone here
|
# Override the default UTC time zone here.
|
||||||
|
# See https://docs.djangoproject.com/en/1.10/ref/settings/#std:setting-TIME_ZONE
|
||||||
|
# for details on how to set it.
|
||||||
#PAPERLESS_TIME_ZONE=UTC
|
#PAPERLESS_TIME_ZONE=UTC
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Django==1.10.5
|
Django>=1.11,<2.0
|
||||||
Pillow>=3.1.1
|
Pillow>=3.1.1
|
||||||
django-crispy-forms>=1.6.1
|
django-crispy-forms>=1.6.1
|
||||||
django-extensions>=1.7.6
|
django-extensions>=1.7.6
|
||||||
@@ -13,12 +13,14 @@ python-dateutil>=2.6.0
|
|||||||
python-dotenv>=0.6.2
|
python-dotenv>=0.6.2
|
||||||
python-gnupg>=0.3.9
|
python-gnupg>=0.3.9
|
||||||
pytz>=2016.10
|
pytz>=2016.10
|
||||||
gunicorn==19.6.0
|
gunicorn==19.7.1
|
||||||
|
|
||||||
# For the tests
|
# For the tests
|
||||||
|
factory-boy
|
||||||
pytest
|
pytest
|
||||||
pytest-django
|
pytest-django
|
||||||
pytest-sugar
|
pytest-sugar
|
||||||
pep8
|
pytest-env
|
||||||
|
pycodestyle
|
||||||
flake8
|
flake8
|
||||||
tox
|
tox
|
||||||
|
@@ -25,16 +25,16 @@ set_permissions() {
|
|||||||
echo "failed."
|
echo "failed."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Either try to set it on your host-mounted directory"
|
echo "Either try to set it on your host-mounted directory"
|
||||||
echo "directly, or make sure that the directory has \`o+x\`"
|
echo "directly, or make sure that the directory has \`g+wx\`"
|
||||||
echo "permissions and the files in it at least \`o+r\`."
|
echo "permissions and the files in it at least \`o+r\`."
|
||||||
} >&2
|
} >&2
|
||||||
chmod g+x "${!dir}" || {
|
chmod g+wx "${!dir}" || {
|
||||||
echo "Changing group permissions of ${cur_dir_name} directory:"
|
echo "Changing group permissions of ${cur_dir_name} directory:"
|
||||||
echo " ${!dir}"
|
echo " ${!dir}"
|
||||||
echo "failed."
|
echo "failed."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Either try to set it on your host-mounted directory"
|
echo "Either try to set it on your host-mounted directory"
|
||||||
echo "directly, or make sure that the directory has \`o+x\`"
|
echo "directly, or make sure that the directory has \`g+wx\`"
|
||||||
echo "permissions and the files in it at least \`o+r\`."
|
echo "permissions and the files in it at least \`o+r\`."
|
||||||
} >&2
|
} >&2
|
||||||
done
|
done
|
||||||
|
@@ -70,9 +70,14 @@ class DocumentAdmin(CommonAdmin):
|
|||||||
created_.short_description = "Created"
|
created_.short_description = "Created"
|
||||||
|
|
||||||
def thumbnail(self, obj):
|
def thumbnail(self, obj):
|
||||||
|
if settings.FORCE_SCRIPT_NAME:
|
||||||
|
src_link = "{}/fetch/thumb/{}".format(
|
||||||
|
settings.FORCE_SCRIPT_NAME, obj.id)
|
||||||
|
else:
|
||||||
|
src_link = "/fetch/thumb/{}".format(obj.id)
|
||||||
png_img = self._html_tag(
|
png_img = self._html_tag(
|
||||||
"img",
|
"img",
|
||||||
src="/fetch/thumb/{}".format(obj.id),
|
src=src_link,
|
||||||
width=180,
|
width=180,
|
||||||
alt="Thumbnail of {}".format(obj.file_name),
|
alt="Thumbnail of {}".format(obj.file_name),
|
||||||
title=obj.file_name
|
title=obj.file_name
|
||||||
|
@@ -38,6 +38,9 @@ class GnuPG(object):
|
|||||||
|
|
||||||
def move_documents_and_create_thumbnails(apps, schema_editor):
|
def move_documents_and_create_thumbnails(apps, schema_editor):
|
||||||
|
|
||||||
|
os.makedirs(os.path.join(settings.MEDIA_ROOT, "documents", "originals"), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(settings.MEDIA_ROOT, "documents", "thumbnails"), exist_ok=True)
|
||||||
|
|
||||||
documents = os.listdir(os.path.join(settings.MEDIA_ROOT, "documents"))
|
documents = os.listdir(os.path.join(settings.MEDIA_ROOT, "documents"))
|
||||||
|
|
||||||
if set(documents) == {"originals", "thumbnails"}:
|
if set(documents) == {"originals", "thumbnails"}:
|
||||||
|
21
src/documents/migrations/0018_auto_20170715_1712.py
Normal file
21
src/documents/migrations/0018_auto_20170715_1712.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-07-15 17:12
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '0017_auto_20170512_0507'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='correspondent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.Correspondent'),
|
||||||
|
),
|
||||||
|
]
|
@@ -1,3 +1,5 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -89,7 +91,7 @@ class MatchingModel(models.Model):
|
|||||||
search_kwargs = {"flags": re.IGNORECASE}
|
search_kwargs = {"flags": re.IGNORECASE}
|
||||||
|
|
||||||
if self.matching_algorithm == self.MATCH_ALL:
|
if self.matching_algorithm == self.MATCH_ALL:
|
||||||
for word in self.match.split(" "):
|
for word in self._split_match():
|
||||||
search_result = re.search(
|
search_result = re.search(
|
||||||
r"\b{}\b".format(word), text, **search_kwargs)
|
r"\b{}\b".format(word), text, **search_kwargs)
|
||||||
if not search_result:
|
if not search_result:
|
||||||
@@ -97,7 +99,7 @@ class MatchingModel(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if self.matching_algorithm == self.MATCH_ANY:
|
if self.matching_algorithm == self.MATCH_ANY:
|
||||||
for word in self.match.split(" "):
|
for word in self._split_match():
|
||||||
if re.search(r"\b{}\b".format(word), text, **search_kwargs):
|
if re.search(r"\b{}\b".format(word), text, **search_kwargs):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -121,6 +123,21 @@ class MatchingModel(models.Model):
|
|||||||
|
|
||||||
raise NotImplementedError("Unsupported matching algorithm")
|
raise NotImplementedError("Unsupported matching algorithm")
|
||||||
|
|
||||||
|
def _split_match(self):
|
||||||
|
"""
|
||||||
|
Splits the match to individual keywords, getting rid of unnecessary
|
||||||
|
spaces and grouping quoted words together.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
' some random words "with quotes " and spaces'
|
||||||
|
==>
|
||||||
|
["some", "random", "words", "with\s+quotes", "and", "spaces"]
|
||||||
|
"""
|
||||||
|
findterms = re.compile(r'"([^"]+)"|(\S+)').findall
|
||||||
|
normspace = re.compile(r"\s+").sub
|
||||||
|
return [normspace(r"\s+", (t[0] or t[1]).strip())
|
||||||
|
for t in findterms(self.match)]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
self.match = self.match.lower()
|
self.match = self.match.lower()
|
||||||
@@ -172,7 +189,12 @@ class Document(models.Model):
|
|||||||
TYPES = (TYPE_PDF, TYPE_PNG, TYPE_JPG, TYPE_GIF, TYPE_TIF,)
|
TYPES = (TYPE_PDF, TYPE_PNG, TYPE_JPG, TYPE_GIF, TYPE_TIF,)
|
||||||
|
|
||||||
correspondent = models.ForeignKey(
|
correspondent = models.ForeignKey(
|
||||||
Correspondent, blank=True, null=True, related_name="documents")
|
Correspondent,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="documents",
|
||||||
|
on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
|
|
||||||
title = models.CharField(max_length=128, blank=True, db_index=True)
|
title = models.CharField(max_length=128, blank=True, db_index=True)
|
||||||
|
|
||||||
@@ -316,45 +338,45 @@ class FileInfo(object):
|
|||||||
r"(?P<correspondent>.*) - "
|
r"(?P<correspondent>.*) - "
|
||||||
r"(?P<title>.*) - "
|
r"(?P<title>.*) - "
|
||||||
r"(?P<tags>[a-z0-9\-,]*)"
|
r"(?P<tags>[a-z0-9\-,]*)"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
)),
|
)),
|
||||||
("created-title-tags", re.compile(
|
("created-title-tags", re.compile(
|
||||||
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
|
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
|
||||||
r"(?P<title>.*) - "
|
r"(?P<title>.*) - "
|
||||||
r"(?P<tags>[a-z0-9\-,]*)"
|
r"(?P<tags>[a-z0-9\-,]*)"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
)),
|
)),
|
||||||
("created-correspondent-title", re.compile(
|
("created-correspondent-title", re.compile(
|
||||||
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
|
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
|
||||||
r"(?P<correspondent>.*) - "
|
r"(?P<correspondent>.*) - "
|
||||||
r"(?P<title>.*)"
|
r"(?P<title>.*)"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
)),
|
)),
|
||||||
("created-title", re.compile(
|
("created-title", re.compile(
|
||||||
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
|
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
|
||||||
r"(?P<title>.*)"
|
r"(?P<title>.*)"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
)),
|
)),
|
||||||
("correspondent-title-tags", re.compile(
|
("correspondent-title-tags", re.compile(
|
||||||
r"(?P<correspondent>.*) - "
|
r"(?P<correspondent>.*) - "
|
||||||
r"(?P<title>.*) - "
|
r"(?P<title>.*) - "
|
||||||
r"(?P<tags>[a-z0-9\-,]*)"
|
r"(?P<tags>[a-z0-9\-,]*)"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
)),
|
)),
|
||||||
("correspondent-title", re.compile(
|
("correspondent-title", re.compile(
|
||||||
r"(?P<correspondent>.*) - "
|
r"(?P<correspondent>.*) - "
|
||||||
r"(?P<title>.*)?"
|
r"(?P<title>.*)?"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
)),
|
)),
|
||||||
("title", re.compile(
|
("title", re.compile(
|
||||||
r"(?P<title>.*)"
|
r"(?P<title>.*)"
|
||||||
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
|
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
))
|
))
|
||||||
])
|
])
|
||||||
@@ -397,6 +419,8 @@ class FileInfo(object):
|
|||||||
r = extension.lower()
|
r = extension.lower()
|
||||||
if r == "jpeg":
|
if r == "jpeg":
|
||||||
return "jpg"
|
return "jpg"
|
||||||
|
if r == "tif":
|
||||||
|
return "tiff"
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
{% load hacks %}
|
|
||||||
|
|
||||||
{# See documents.templatetags.hacks.change_list_results for an explanation #}
|
|
||||||
|
|
||||||
{% change_list_results %}
|
|
||||||
|
|
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'admin/change_form.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
{# Hack to force Django to make the created date a date input rather than `text` (the default) #}
|
||||||
|
<script>
|
||||||
|
django.jQuery(".field-created input").first().attr("type", "date")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock footer %}
|
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'admin/change_list.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
{% load admin_actions from admin_list%}
|
||||||
|
{% load result_list from hacks %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block result_list %}
|
||||||
|
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
|
||||||
|
{% result_list cl %}
|
||||||
|
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
|
||||||
|
{% endblock %}
|
@@ -29,18 +29,13 @@
|
|||||||
.result .header {
|
.result .header {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: #79AEC8;
|
background-color: #79AEC8;
|
||||||
height: 6em;
|
|
||||||
}
|
|
||||||
.result .header .checkbox {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
}
|
||||||
.result .header .checkbox{
|
.result .header .checkbox{
|
||||||
width: 5%;
|
width: 5%;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
.result .header .info {
|
.result .header .info {
|
||||||
width: 90%;
|
margin-left: 10%;
|
||||||
float: left;
|
|
||||||
}
|
}
|
||||||
.result .header a,
|
.result .header a,
|
||||||
.result a.tag {
|
.result a.tag {
|
||||||
|
@@ -6,5 +6,6 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{# One day someone (maybe even myself) is going to write a proper web front-end for Paperless, and this is where it'll start. #}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,41 +1,28 @@
|
|||||||
import os
|
from django.contrib.admin.templatetags.admin_list import (
|
||||||
|
result_headers,
|
||||||
from django.contrib import admin
|
result_hidden_fields,
|
||||||
|
results
|
||||||
|
)
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
from django.template.loader import get_template
|
|
||||||
|
|
||||||
from ..models import Document
|
|
||||||
|
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.inclusion_tag("admin/documents/document/change_list_results.html")
|
||||||
def change_list_results(context):
|
def result_list(cl):
|
||||||
"""
|
"""
|
||||||
Django has a lot of places where you can override defaults, but
|
Copy/pasted from django.contrib.admin.templatetags.admin_list just so I can
|
||||||
unfortunately, `change_list_results.html` is not one of them. In fact,
|
modify the value passed to `.inclusion_tag()` in the decorator here. There
|
||||||
it's a downright pain in the ass to override this file on a per-model basis
|
must be a cleaner way... right?
|
||||||
and this is the cleanest way I could come up with.
|
|
||||||
|
|
||||||
Basically all we've done here is defined `change_list_results.html` in an
|
|
||||||
`admin` directory which globally overrides that file for *every* model.
|
|
||||||
That template however simply loads this templatetag which determines
|
|
||||||
whether we're currently looking at a `Document` listing or something else
|
|
||||||
and loads the appropriate file in each case.
|
|
||||||
|
|
||||||
Better work arounds for this are welcome as I hate this myself, but at the
|
|
||||||
moment, it's all I could come up with.
|
|
||||||
"""
|
"""
|
||||||
|
headers = list(result_headers(cl))
|
||||||
path = os.path.join(
|
num_sorted_fields = 0
|
||||||
os.path.dirname(admin.__file__),
|
for h in headers:
|
||||||
"templates",
|
if h['sortable'] and h['sorted']:
|
||||||
"admin",
|
num_sorted_fields += 1
|
||||||
"change_list_results.html"
|
return {'cl': cl,
|
||||||
)
|
'result_hidden_fields': list(result_hidden_fields(cl)),
|
||||||
|
'result_headers': headers,
|
||||||
if context["cl"].model == Document:
|
'num_sorted_fields': num_sorted_fields,
|
||||||
path = "admin/documents/document/change_list_results.html"
|
'results': list(results(cl))}
|
||||||
|
|
||||||
return get_template(path).render(context)
|
|
||||||
|
17
src/documents/tests/factories.py
Normal file
17
src/documents/tests/factories.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import factory
|
||||||
|
|
||||||
|
from ..models import Document, Correspondent
|
||||||
|
|
||||||
|
|
||||||
|
class CorrespondentFactory(factory.DjangoModelFactory):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Correspondent
|
||||||
|
|
||||||
|
name = factory.Faker("name")
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentFactory(factory.DjangoModelFactory):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Document
|
@@ -58,9 +58,9 @@ class TestAttributes(TestCase):
|
|||||||
|
|
||||||
TAGS = ("tag1", "tag2", "tag3")
|
TAGS = ("tag1", "tag2", "tag3")
|
||||||
EXTENSIONS = (
|
EXTENSIONS = (
|
||||||
"pdf", "png", "jpg", "jpeg", "gif",
|
"pdf", "png", "jpg", "jpeg", "gif", "tiff", "tif",
|
||||||
"PDF", "PNG", "JPG", "JPEG", "GIF",
|
"PDF", "PNG", "JPG", "JPEG", "GIF", "TIFF", "TIF",
|
||||||
"PdF", "PnG", "JpG", "JPeG", "GiF",
|
"PdF", "PnG", "JpG", "JPeG", "GiF", "TiFf", "TiF",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _test_guess_attributes_from_name(self, path, sender, title, tags):
|
def _test_guess_attributes_from_name(self, path, sender, title, tags):
|
||||||
@@ -80,6 +80,8 @@ class TestAttributes(TestCase):
|
|||||||
self.assertEqual(tuple([t.slug for t in file_info.tags]), tags, f)
|
self.assertEqual(tuple([t.slug for t in file_info.tags]), tags, f)
|
||||||
if extension.lower() == "jpeg":
|
if extension.lower() == "jpeg":
|
||||||
self.assertEqual(file_info.extension, "jpg", f)
|
self.assertEqual(file_info.extension, "jpg", f)
|
||||||
|
elif extension.lower() == "tif":
|
||||||
|
self.assertEqual(file_info.extension, "tiff", f)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(file_info.extension, extension.lower(), f)
|
self.assertEqual(file_info.extension, extension.lower(), f)
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from ..models import Correspondent, Document, Tag
|
from ..models import Correspondent, Document, Tag
|
||||||
from ..signals import document_consumption_finished
|
from ..signals import document_consumption_finished
|
||||||
@@ -16,9 +16,15 @@ class TestMatching(TestCase):
|
|||||||
matching_algorithm=getattr(klass, algorithm)
|
matching_algorithm=getattr(klass, algorithm)
|
||||||
)
|
)
|
||||||
for string in true:
|
for string in true:
|
||||||
self.assertTrue(instance.matches(string))
|
self.assertTrue(
|
||||||
|
instance.matches(string),
|
||||||
|
'"%s" should match "%s" but it does not' % (text, string)
|
||||||
|
)
|
||||||
for string in false:
|
for string in false:
|
||||||
self.assertFalse(instance.matches(string))
|
self.assertFalse(
|
||||||
|
instance.matches(string),
|
||||||
|
'"%s" should not match "%s" but it does' % (text, string)
|
||||||
|
)
|
||||||
|
|
||||||
def test_match_all(self):
|
def test_match_all(self):
|
||||||
|
|
||||||
@@ -54,6 +60,21 @@ class TestMatching(TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._test_matching(
|
||||||
|
'brown fox "lazy dogs"',
|
||||||
|
"MATCH_ALL",
|
||||||
|
(
|
||||||
|
"the quick brown fox jumped over the lazy dogs",
|
||||||
|
"the quick brown fox jumped over the lazy dogs",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"the quick fox jumped over the lazy dogs",
|
||||||
|
"the quick brown wolf jumped over the lazy dogs",
|
||||||
|
"the quick brown fox jumped over the fat dogs",
|
||||||
|
"the quick brown fox jumped over the lazy... dogs",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_match_any(self):
|
def test_match_any(self):
|
||||||
|
|
||||||
self._test_matching(
|
self._test_matching(
|
||||||
@@ -89,6 +110,18 @@ class TestMatching(TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._test_matching(
|
||||||
|
'"brown fox" " lazy dogs "',
|
||||||
|
"MATCH_ANY",
|
||||||
|
(
|
||||||
|
"the quick brown fox",
|
||||||
|
"jumped over the lazy dogs.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"the lazy fox jumped over the brown dogs",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_match_literal(self):
|
def test_match_literal(self):
|
||||||
|
|
||||||
self._test_matching(
|
self._test_matching(
|
||||||
@@ -166,7 +199,8 @@ class TestMatching(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestApplications(TestCase):
|
@override_settings(POST_CONSUME_SCRIPT=None)
|
||||||
|
class TestDocumentConsumptionFinishedSignal(TestCase):
|
||||||
"""
|
"""
|
||||||
We make use of document_consumption_finished, so we should test that it's
|
We make use of document_consumption_finished, so we should test that it's
|
||||||
doing what we expect wrt to tag & correspondent matching.
|
doing what we expect wrt to tag & correspondent matching.
|
||||||
|
31
src/documents/tests/test_models.py
Normal file
31
src/documents/tests/test_models.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from ..models import Document, Correspondent
|
||||||
|
from .factories import DocumentFactory, CorrespondentFactory
|
||||||
|
|
||||||
|
|
||||||
|
class CorrespondentTestCase(TestCase):
|
||||||
|
|
||||||
|
def test___str__(self):
|
||||||
|
for s in ("test", "οχι", "test with fun_charÅc'\"terß"):
|
||||||
|
correspondent = CorrespondentFactory.create(name=s)
|
||||||
|
self.assertEqual(str(correspondent), s)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_correspondent_deletion_does_not_cascade(self):
|
||||||
|
|
||||||
|
self.assertEqual(Correspondent.objects.all().count(), 0)
|
||||||
|
correspondent = CorrespondentFactory.create()
|
||||||
|
self.assertEqual(Correspondent.objects.all().count(), 1)
|
||||||
|
|
||||||
|
self.assertEqual(Document.objects.all().count(), 0)
|
||||||
|
DocumentFactory.create(correspondent=correspondent)
|
||||||
|
self.assertEqual(Document.objects.all().count(), 1)
|
||||||
|
self.assertIsNotNone(Document.objects.all().first().correspondent)
|
||||||
|
|
||||||
|
correspondent.delete()
|
||||||
|
self.assertEqual(Correspondent.objects.all().count(), 0)
|
||||||
|
self.assertEqual(Document.objects.all().count(), 1)
|
||||||
|
self.assertIsNone(Document.objects.all().first().correspondent)
|
@@ -47,6 +47,7 @@ _allowed_hosts = os.getenv("PAPERLESS_ALLOWED_HOSTS")
|
|||||||
if _allowed_hosts:
|
if _allowed_hosts:
|
||||||
ALLOWED_HOSTS = _allowed_hosts.split(",")
|
ALLOWED_HOSTS = _allowed_hosts.split(",")
|
||||||
|
|
||||||
|
FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME")
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@@ -1,13 +1,17 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import url, static, include
|
from django.conf.urls import include, static, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.generic import RedirectView
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from documents.views import (
|
from documents.views import (
|
||||||
FetchView, PushView,
|
CorrespondentViewSet,
|
||||||
CorrespondentViewSet, TagViewSet, DocumentViewSet, LogViewSet
|
DocumentViewSet,
|
||||||
|
FetchView,
|
||||||
|
LogViewSet,
|
||||||
|
PushView,
|
||||||
|
TagViewSet
|
||||||
)
|
)
|
||||||
from reminders.views import ReminderViewSet
|
from reminders.views import ReminderViewSet
|
||||||
|
|
||||||
@@ -39,7 +43,9 @@ urlpatterns = [
|
|||||||
|
|
||||||
# The Django admin
|
# The Django admin
|
||||||
url(r"admin/", admin.site.urls),
|
url(r"admin/", admin.site.urls),
|
||||||
url(r"", admin.site.urls), # This is going away
|
|
||||||
|
# Catch all redirect back to /admin
|
||||||
|
url(r"", RedirectView.as_view(permanent=True, url="/admin/")),
|
||||||
|
|
||||||
] + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
@@ -1 +1 @@
|
|||||||
__version__ = (0, 6, 1)
|
__version__ = (1, 0, 0)
|
||||||
|
@@ -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|jpe?g|gif|png|tiff?|pnm|bmp)$")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle(cls, sender, **kwargs):
|
def handle(cls, sender, **kwargs):
|
||||||
|
@@ -12,9 +12,9 @@ class SignalsTestCase(TestCase):
|
|||||||
"A document with a . in it", "Doc with -- in it"
|
"A document with a . in it", "Doc with -- in it"
|
||||||
)
|
)
|
||||||
suffixes = (
|
suffixes = (
|
||||||
"pdf", "jpg", "gif", "png", "tiff", "tif", "pnm", "bmp",
|
"pdf", "jpg", "jpeg", "gif", "png", "tiff", "tif", "pnm", "bmp",
|
||||||
"PDF", "JPG", "GIF", "PNG", "TIFF", "TIF", "PNM", "BMP",
|
"PDF", "JPG", "JPEG", "GIF", "PNG", "TIFF", "TIF", "PNM", "BMP",
|
||||||
"pDf", "jPg", "gIf", "pNg", "tIff", "tIf", "pNm", "bMp",
|
"pDf", "jPg", "jpEg", "gIf", "pNg", "tIff", "tIf", "pNm", "bMp",
|
||||||
)
|
)
|
||||||
|
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE=paperless.settings
|
DJANGO_SETTINGS_MODULE=paperless.settings
|
||||||
|
addopts = --pythonwarnings=all
|
||||||
|
env =
|
||||||
|
PAPERLESS_CONSUME=/tmp
|
||||||
|
PAPERLESS_PASSPHRASE=THISISNOTASECRET
|
||||||
|
PAPERLESS_SECRET=paperless
|
||||||
|
PAPERLESS_EMAIL_SECRET=paperless
|
||||||
|
21
src/tox.ini
21
src/tox.ini
@@ -5,19 +5,18 @@
|
|||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
envlist = py34, py35, py36, pep8
|
envlist = py34, py35, py36, pycodestyle
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = {envpython} manage.py test
|
commands = pytest
|
||||||
deps = -r{toxinidir}/../requirements.txt
|
deps = -r{toxinidir}/../requirements.txt
|
||||||
setenv =
|
|
||||||
PAPERLESS_CONSUME=/tmp
|
|
||||||
PAPERLESS_PASSPHRASE=THISISNOTASECRET
|
|
||||||
PAPERLESS_SECRET=paperless
|
|
||||||
|
|
||||||
[testenv:pep8]
|
[testenv:pycodestyle]
|
||||||
commands=pep8
|
commands=pycodestyle
|
||||||
deps=pep8
|
deps=pycodestyle
|
||||||
|
|
||||||
[pep8]
|
[pycodestyle]
|
||||||
exclude=.tox,migrations,paperless/settings.py
|
exclude=
|
||||||
|
.tox,
|
||||||
|
migrations,
|
||||||
|
paperless/settings.py
|
||||||
|
Reference in New Issue
Block a user