Compare commits

..

66 Commits
0.5.0 ... 1.0.0

Author SHA1 Message Date
Daniel Quinn
3ca215e4dc Bump to v1.0.0! 2018-01-06 19:25:33 +00:00
Daniel Quinn
16c4183333 Upgrade to Django 1.11.x 2018-01-06 19:24:10 +00:00
Daniel Quinn
6fe37678f2 Change date fields to actual date fields #278 2018-01-06 19:21:49 +00:00
Daniel Quinn
b58188f805 Switch from pep8 to pycodestyle 2018-01-06 18:56:37 +00:00
Daniel Quinn
f2a42ab6fe Add catch-all redirect for /admin/ 2018-01-06 18:51:16 +00:00
Daniel Quinn
e236b7bf7b isort 2018-01-06 18:51:10 +00:00
Daniel Quinn
35004f434b Add a smarter work-around for the change-list-results hack 2018-01-06 18:47:01 +00:00
Daniel Quinn
75251ad694 Add a note for future development 2018-01-06 18:30:33 +00:00
Daniel Quinn
870357968a Fix tests to run on boxes with post-consume-scripts set 2018-01-06 17:23:24 +00:00
Daniel Quinn
a593798b4b Add encoding declaration 2018-01-06 17:23:07 +00:00
Daniel Quinn
4f070ba162 Use double quotes by default 2018-01-06 17:22:57 +00:00
Daniel Quinn
9517d27f40 Add warnings to the test runner 2018-01-06 17:22:40 +00:00
Daniel Quinn
35bb3dbcc2 Clean up CSS for #272 2018-01-06 15:57:25 +00:00
Daniel Quinn
06117929bb Merge pull request #277 from ishirav/multi-word-match
Add multi-word match
2017-12-27 11:21:27 +01:00
ishirav
d1c8241947 break long lines (pep8) 2017-12-23 07:39:40 +02:00
ishirav
4c38b28469 break long lines (pep8) 2017-12-23 06:59:48 +02:00
ishirav
ad0f0a0b5d Add documentation about multi-word search terms 2017-12-23 06:44:06 +02:00
ishirav
83746a9aeb Add tests and improve whitespace handling 2017-12-23 06:37:00 +02:00
ishirav
6a36a4ec97 Support search terms that contain multiple words in ANY/ALL matching modes, by surrounding the terms with double quotes. 2017-12-23 06:05:48 +02:00
Daniel Quinn
af4623e605 Merge pull request #270 from dev-rke/patch-2
#248: fix missing CSS
I'm not thrilled about this and would much rather have Nginx running to do the job, but I just don't have the time to do that right now.  As Pit says, this is better than leaving DEBUG on.
2017-11-05 20:45:28 +00:00
Daniel Quinn
db8e116681 Merge pull request #269 from dev-rke/patch-1
Change default /consume volume
2017-11-05 20:42:08 +00:00
dev-rke
a8616ebfe2 #248: fix missing CSS
Force the server to use --insecure flag to also provide static contents like CSS files.
See #248 and #167 for more details.
2017-11-04 16:02:29 +01:00
dev-rke
a38d3bf7f8 Change default /consume volume
Change default /consume volume to ./consume on your host, so that no unexpected folder will be generated on the host machine.
2017-11-04 15:57:21 +01:00
Daniel Quinn
1cb5bbd07d Merge pull request #268 from pitkley/267-dir-permissions
Set `g+w` on the consumption/export directories
2017-11-01 11:30:16 +00:00
Daniel Quinn
6edb5b912f Move all scanner recommendations to new doc page 2017-11-01 11:24:11 +00:00
Daniel Quinn
ec20c7577e Add a new page for scanner recommendations 2017-11-01 11:15:37 +00:00
Daniel Quinn
d6df9b3656 Strip whitespace 2017-11-01 11:15:22 +00:00
Pit Kleyersburg
80a849fef7 Set g+w on the consumption/export directories
This should fix issue #267.
2017-10-31 15:30:33 +01:00
Daniel Quinn
bd67b53d50 Update test for #259 fix 2017-10-16 10:53:18 +01:00
Daniel Quinn
e32ed09da3 Support .jpeg as well as .jpg 2017-10-16 09:00:38 +01:00
Daniel Quinn
c5632e5c04 Update changelog for 0.8.0 2017-09-10 12:51:00 +01:00
Daniel Quinn
4d2b71454d Ignore .virtualenv 2017-09-09 12:22:03 +03:00
Daniel Quinn
5cbb33b02b Add documentation for the new FORCE_SCRIPT_NAME feature 2017-09-09 12:21:31 +03:00
Daniel Quinn
2c55aad6c0 Merge pull request #255 from maphy-psd/master
add FORCE_SCRIPT_NAME to host paperless on a subpath url
2017-09-06 15:56:44 +01:00
Daniel Quinn
1e039dcb32 Bump gunicorn 2017-08-30 00:44:13 +03:00
Daniel Quinn
6ca8da4858 Patch requirements to keep up with Django versions 2017-08-30 00:27:54 +03:00
maphy-psd
82f05e27c3 fix travis ci E510
E501 line too long (85 > 79 characters)
2017-08-20 16:18:39 +02:00
maphy-psd
7a627e4ad8 white spacing and remove var's prefix 2017-08-20 14:29:51 +02:00
maphy-psd
73af9552ec getenv has "None" as default
@MasterofJOKers in PR#255
2017-08-20 14:13:23 +02:00
maphy-psd
e4854f2144 def thumbnail uses FORCE_SCRIPT_NAME
with this edit the tumbnails are show up..
2017-08-19 18:37:17 +02:00
maphy-psd
6f5c1ac4e1 add FORCE_SCRIPT_NAME setting 2017-08-19 12:39:25 +02:00
maphy-psd
22acc51284 add PAPERLESS_FORCE_SCRIPT_NAME 2017-08-19 12:38:45 +02:00
Daniel Quinn
a05644fc31 Merge pull request #250 from brightdroid/master
create documents subfolder folder if they do not exist
2017-08-12 14:39:22 +01:00
Christoph Roeder
d1aa54caa9 create documents subfolder folder if they do not exist 2017-07-31 21:35:41 +02:00
Daniel Quinn
e293f70a91 Merge pull request #247 from danielquinn/issue/235
Allow correspondents to be deleted without deleting their documents
2017-07-15 19:41:33 +01:00
Daniel Quinn
347986a2b3 Allow correspondents to be deleted without deleting their documents
Fixes #235
2017-07-15 19:13:10 +01:00
Daniel Quinn
ede274386b Detect .tif files properly
Fixes #232
2017-07-15 19:02:11 +01:00
Daniel Quinn
3e083354cc Merge pull request #246 from kskyten/vb_memory
Add memory to the virtual machine
2017-07-10 15:02:45 +01:00
Kusti Skytén
b2b4f6516a Add memory to the virtual machine
Fixes #244
2017-07-10 16:55:51 +03:00
Daniel Quinn
2ae702c7bb Merge pull request #245 from tooomm/patch-1
README: unify badges (versioneye)
2017-07-09 18:25:28 +01:00
tooomm
b748420a94 unify badges (versioneye)
normal > flat style
2017-07-09 15:17:42 +02:00
Daniel Quinn
8a4546ce0d Merge pull request #242 from MasterofJOKers/setup_collectstatic
Mention "collectstatic" in the docs
2017-06-27 13:07:07 +01:00
MasterofJOKers
167412a003 Mention "collectstatic" in the docs
When using the built-in webserver in debug mode, the static files are
handled automatically. From the Django docs:

	During development, if you use django.contrib.staticfiles, this will
	be done automatically by runserver when DEBUG is set to True (see
	django.contrib.staticfiles.views.serve()).

	This method is grossly inefficient and probably insecure, so it is
	unsuitable for production.

This means, when using a real webserver, it also has to serve the static
files, i.e.  CSS and JavaScript. For that, one needs to run `./manage.py
collectstatic` first.
2017-06-26 17:08:37 +02:00
Daniel Quinn
e8d90b42a1 Merge pull request #240 from ddddavidmartin/timezone_documentation_clarification
Add link to Django documentation for time zone setting in example config.
2017-06-24 09:24:42 +01:00
David Martin
d8c7e9de5f Add link to documentation for time zone setting in example config.
It is not obvious which time zones the option in the config file
accepts. Having a link to the official django documentation makes it
clear.
2017-06-24 12:27:26 +10:00
Daniel Quinn
2ac1b78a2c Move testing ENV vars into pytest.ini 2017-06-19 10:57:30 +01:00
Daniel Quinn
e8e38befb7 Fix test for new email secret 2017-06-19 10:24:23 +01:00
Daniel Quinn
b30629dd60 Remove debugging info 2017-06-19 09:22:26 +01:00
Daniel Quinn
f66d7e1c2d Drop SHARED_SECRET in favour of EMAIL_SECRET
Originally we used SHARED secret both for email and for the API.  That
was a bad idea, and now that we're only using this value for one case,
I've renamed it to reflect its actual use.
2017-06-18 22:08:42 +01:00
Daniel Quinn
8417ac7eeb Merge pull request #237 from danielquinn/fix-http-post
Fix http post
2017-06-13 17:52:48 +01:00
Daniel Quinn
6342225b22 Merge pull request #238 from Strubbl/fix-shellcheck-issues
docker-entrypoint.sh: fix shellcheck issues
2017-06-13 17:51:49 +01:00
Sven Fischer
4460fb7004 docker-entrypoint.sh: fix shellcheck issues
issues found by shellcheck were:

```
$ shellcheck docker-entrypoint.sh

In docker-entrypoint.sh line 10:
    if [[ ${USERMAP_UID} != ${USERMAP_ORIG_UID} || ${USERMAP_GID} != ${USERMAP_ORIG_GID} ]]; then
                            ^-- SC2053: Quote the rhs of != in [[ ]] to prevent glob matching.
                                                                     ^-- SC2053: Quote the rhs of != in [[ ]] to prevent glob matching.

In docker-entrypoint.sh line 12:
        groupmod -g ${USERMAP_GID} paperless
                    ^-- SC2086: Double quote to prevent globbing and word splitting.

In docker-entrypoint.sh line 65:
        if dpkg -s "$pkg" 2>&1 > /dev/null; then
                          ^-- SC2069: The order of the 2>&1 and the redirect matters. The 2>&1 has to be last.

In docker-entrypoint.sh line 69:
        if ! apt-cache show "$pkg" 2>&1 > /dev/null; then
                                   ^-- SC2069: The order of the 2>&1 and the redirect matters. The 2>&1 has to be last.
```
2017-06-12 21:09:59 +02:00
Daniel Quinn
6f635c74fc Fix HTTP POST of documents
After tinkering with this for about 2 hours, I'm reasonably sure this
ever worked.  This feature was added by me in haste and poked by by the
occasional contributor, and it suffered from neglect.

* Removed the requirement for signature generation in favour of simply
  requiring BasicAuth or a valid session id.
* Fixed a number of bugs in the form itself that would have ensured that
  the form never accepted anything.
* Documented it all properly so now (hopefully) people will have less
  trouble figuring it out in the future.
2017-06-11 01:23:37 +01:00
Daniel Quinn
c82d45689c Remove unused imports & comments 2017-06-11 01:23:08 +01:00
Daniel Quinn
02e0543a02 Merge pull request #233 from lucaskolstad/django_filters_installed_app
Add django_filters to INSTALLED_APPS
2017-05-31 10:39:49 +01:00
Lucas Kolstad
fde0276d65 Add django_filters to INSTALLED_APPS 2017-05-30 15:05:34 -07:00
38 changed files with 506 additions and 215 deletions

1
.gitignore vendored
View File

@@ -68,6 +68,7 @@ db.sqlite3
.idea
# Other stuff that doesn't belong
.virtualenv
virtualenv
.vagrant
docker-compose.yml

View File

@@ -26,7 +26,8 @@ How it Works
Paperless does not control your scanner, it only helps you deal with what your
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.
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
@@ -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
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
.. _this other one: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
.. _scanner recommendations: https://paperless.readthedocs.io/en/latest/scanners.html
.. _ImageMagick: http://imagemagick.org/
.. _Tesseract: https://github.com/tesseract-ocr
.. _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
.. |Travis| image:: https://travis-ci.org/danielquinn/paperless.svg?branch=master
: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

5
Vagrantfile vendored
View File

@@ -12,4 +12,9 @@ Vagrant.configure(VAGRANT_API_VERSION) do |config|
# Networking details
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

View File

@@ -17,7 +17,7 @@ services:
# value with nothing.
environment:
- PAPERLESS_OCR_LANGUAGES=
command: ["runserver", "0.0.0.0:8000"]
command: ["runserver", "--insecure", "0.0.0.0:8000"]
consumer:
image: pitkley/paperless
@@ -26,7 +26,7 @@ services:
- media:/usr/src/paperless/media
# You have to adapt the local path you want the consumption
# 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
# exporting. This is not strictly needed for paperless to
# function, only if you're exporting your files: uncomment

View File

@@ -1,6 +1,49 @@
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
* Abandon the shared-secret trick we were using for the POST API in favour
of BasicAuth or Django session.
* Fix the POST API so it actually works. `#236`_
* **Breaking change**: We've dropped the use of ``PAPERLESS_SHARED_SECRET``
as it was being used both for the API (now replaced with a normal auth)
and form email polling. Now that we're only using it for email, this
variable has been renamed to ``PAPERLESS_EMAIL_SECRET``. The old value
will still work for a while, but you should change your config if you've
been using the email polling feature. Thanks to `Joshua Gilman`_ for all
the help with this feature.
* 0.5.0
* Support for fuzzy matching in the auto-tagger & auto-correspondent systems
thanks to `Jake Gysland`_'s patch `#220`_.
@@ -11,7 +54,8 @@ Changelog
thanks to `CkuT`_ for finding this shortcoming and doing the work to get
it fixed in `#224`_.
* All of the following changes are thanks to `David Martin`_:
* Bumped the dependency on pyocr to 0.4.7 so new users can make use of Tesseract 4 if they so prefer (`#226`_).
* Bumped the dependency on pyocr to 0.4.7 so new users can make use of
Tesseract 4 if they so prefer (`#226`_).
* Fixed a number of issues with the automated mail handler (`#227`_, `#228`_)
* Amended the documentation for better handling of systemd service files (`#229`_)
* Amended the Django Admin configuration to have nice headers (`#230`_)
@@ -206,6 +250,14 @@ Changelog
.. _CkuT: https://github.com/CkuT
.. _David Martin: https://github.com/ddddavidmartin
.. _Paperless Desktop: https://github.com/thomasbrueggemann/paperless-desktop
.. _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
.. _#44: https://github.com/danielquinn/paperless/issues/44
@@ -243,4 +295,12 @@ Changelog
.. _#228: https://github.com/danielquinn/paperless/pull/228
.. _#229: https://github.com/danielquinn/paperless/pull/229
.. _#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
.. _#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

View File

@@ -125,7 +125,7 @@ So, with all that in mind, here's what you do to get it running:
``PATHS AND FOLDERS`` and ``SECURITY``.
If you decided to use a subfolder of an existing account, then make sure you
set ``PAPERLESS_CONSUME_MAIL_INBOX`` accordingly here. You also have to set
the ``PAPERLESS_SHARED_SECRET`` to something you can remember 'cause you'll
the ``PAPERLESS_EMAIL_SECRET`` to something you can remember 'cause you'll
have to include that in every email you send.
3. Restart the :ref:`consumer <utilities-consumer>`. The consumer will check
the configured email account at startup and from then on every 10 minutes
@@ -147,46 +147,83 @@ So, with all that in mind, here's what you do to get it running:
HTTP POST
=========
You can also submit a document via HTTP POST. It doesn't do tags yet, and the
URL schema isn't concrete, but it's a start.
To push your document to Paperless, send an HTTP POST to the server with the
following name/value pairs:
You can also submit a document via HTTP POST, so long as you do so after
authenticating. To push your document to Paperless, send an HTTP POST to the
server with the following name/value pairs:
* ``correspondent``: The name of the document's correspondent. Note that there
are restrictions on what characters you can use here. Specifically,
alphanumeric characters, `-`, `,`, `.`, and `'` are ok, everything else it
alphanumeric characters, `-`, `,`, `.`, and `'` are ok, everything else is
out. You also can't use the sequence ` - ` (space, dash, space).
* ``title``: The title of the document. The rules for characters is the same
here as the correspondent.
* ``signature``: For security reasons, we have the correspondent send a
signature using a "shared secret" method to make sure that random strangers
don't start uploading stuff to your server. The means of generating this
signature is defined below.
* ``document``: The file you're uploading
Specify ``enctype="multipart/form-data"``, and then POST your file with::
Content-Disposition: form-data; name="document"; filename="whatever.pdf"
An example of this in HTML is a typical form:
.. _consumption-http-signature:
.. code:: html
Generating the Signature
------------------------
<form method="post" enctype="multipart/form-data">
<input type="text" name="correspondent" value="My Correspondent" />
<input type="text" name="title" value="My Title" />
<input type="file" name="document" />
<input type="submit" name="go" value="Do the thing" />
</form>
Generating a signature based a shared secret is pretty simple: define a secret,
and store it on the server and the client. Then use that secret, along with
the text you want to verify to generate a string that you can use for
verification.
In the case of Paperless, you configure the server with the secret by setting
``UPLOAD_SHARED_SECRET``. Then on your client, you generate your signature by
concatenating the correspondent, title, and the secret, and then using sha256
to generate a hexdigest.
If you're using Python, this is what that looks like:
But a potentially more useful way to do this would be in Python. Here we use
the requests library to handle basic authentication and to send the POST data
to the URL.
.. code:: python
import os
from hashlib import sha256
signature = sha256(correspondent + title + secret).hexdigest()
import requests
from requests.auth import HTTPBasicAuth
# You authenticate via BasicAuth or with a session id.
# We use BasicAuth here
username = "my-username"
password = "my-super-secret-password"
# Where you have Paperless installed and listening
url = "http://localhost:8000/push"
# Document metadata
correspondent = "Test Correspondent"
title = "Test Title"
# The local file you want to push
path = "/path/to/some/directory/my-document.pdf"
with open(path, "rb") as f:
response = requests.post(
url=url,
data={"title": title, "correspondent": correspondent},
files={"document": (os.path.basename(path), f, "application/pdf")},
auth=HTTPBasicAuth(username, password),
allow_redirects=False
)
if response.status_code == 202:
# Everything worked out ok
print("Upload successful")
else:
# If you don't get a 202, it's probably because your credentials
# are wrong or something. This will give you a rough idea of what
# happened.
print("We got HTTP status code: {}".format(response.status_code))
for k, v in response.headers.items():
print("{}: {}".format(k, v))

View File

@@ -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
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
consumer. Once complete, you should see the newly-created document,
automatically tagged with the appropriate data.

View File

@@ -3,10 +3,10 @@
Paperless
=========
Paperless is a simple Django application running in two parts:
a :ref:`consumer <utilities-consumer>` (the thing that does the indexing) and
Paperless is a simple Django application running in two parts:
a :ref:`consumer <utilities-consumer>` (the thing that does the indexing) and
the :ref:`webserver <utilities-webserver>` (the part that lets you search & download
already-indexed documents). If you want to learn more about its functions keep on
already-indexed documents). If you want to learn more about its functions keep on
reading after the installation section.
@@ -19,8 +19,8 @@ Paper is a nightmare. Environmental issues aside, there's no excuse for it in
the 21st century. It takes up space, collects dust, doesn't support any form of
a search feature, indexing is tedious, it's heavy and prone to damage & loss.
I wrote this to make "going paperless" easier. I do not have to worry about
finding stuff again. I feed documents right from the post box into the scanner and
I wrote this to make "going paperless" easier. I do not have to worry about
finding stuff again. I feed documents right from the post box into the scanner and
then shred them. Perhaps you might find it useful too.
@@ -40,4 +40,5 @@ Contents
guesswork
migrating
troubleshooting
scanners
changelog

29
docs/scanners.rst Normal file
View 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

View File

@@ -394,7 +394,10 @@ Using a Real Webserver
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,
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
~~~~~~
@@ -572,3 +575,28 @@ If you're using Docker, you can set a restart-policy_ in the
Docker daemon.
.. _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

View File

@@ -5,7 +5,7 @@
###############################################################################
#### Paths and folders ####
#### Paths & Folders ####
###############################################################################
# This where your documents should go to be consumed. Make sure that it exists
@@ -39,7 +39,11 @@ PAPERLESS_CONSUME_MAIL_PASS=""
# Override the default IMAP inbox here. If not set Paperless defaults to
# "INBOX".
#PAPERLESS_CONSUME_MAIL_INBOX=""
#PAPERLESS_CONSUME_MAIL_INBOX="INBOX"
# Any email sent to the target account that does not contain this text will be
# ignored.
PAPERLESS_EMAIL_SECRET=""
###############################################################################
@@ -61,11 +65,6 @@ PAPERLESS_CONSUME_MAIL_PASS=""
PAPERLESS_PASSPHRASE="secret"
# If you intend to consume documents either via HTTP POST or by email, you must
# have a shared secret here.
PAPERLESS_SHARED_SECRET=""
# The secret key has a default that should be fine so long as you're hosting
# Paperless on a closed network. However, if you're putting this anywhere
# public, you should change the key to something unique and verbose.
@@ -81,6 +80,11 @@ PAPERLESS_SHARED_SECRET=""
# as is "example.com,www.example.com", but NOT " example.com" or "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 ####
@@ -157,7 +161,9 @@ PAPERLESS_SHARED_SECRET=""
#### 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

View File

@@ -1,4 +1,4 @@
Django==1.10.5
Django>=1.11,<2.0
Pillow>=3.1.1
django-crispy-forms>=1.6.1
django-extensions>=1.7.6
@@ -13,12 +13,14 @@ python-dateutil>=2.6.0
python-dotenv>=0.6.2
python-gnupg>=0.3.9
pytz>=2016.10
gunicorn==19.6.0
gunicorn==19.7.1
# For the tests
factory-boy
pytest
pytest-django
pytest-sugar
pep8
pytest-env
pycodestyle
flake8
tox

View File

@@ -7,9 +7,9 @@ map_uidgid() {
USERMAP_ORIG_UID=$(id -g paperless)
USERMAP_GID=${USERMAP_GID:-${USERMAP_UID:-$USERMAP_ORIG_GID}}
USERMAP_UID=${USERMAP_UID:-$USERMAP_ORIG_UID}
if [[ ${USERMAP_UID} != ${USERMAP_ORIG_UID} || ${USERMAP_GID} != ${USERMAP_ORIG_GID} ]]; then
if [[ ${USERMAP_UID} != "${USERMAP_ORIG_UID}" || ${USERMAP_GID} != "${USERMAP_ORIG_GID}" ]]; then
echo "Mapping UID and GID for paperless:paperless to $USERMAP_UID:$USERMAP_GID"
groupmod -g ${USERMAP_GID} paperless
groupmod -g "${USERMAP_GID}" paperless
sed -i -e "s|:${USERMAP_ORIG_UID}:${USERMAP_GID}:|:${USERMAP_UID}:${USERMAP_GID}:|" /etc/passwd
fi
}
@@ -25,16 +25,16 @@ set_permissions() {
echo "failed."
echo ""
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\`."
} >&2
chmod g+x "${!dir}" || {
chmod g+wx "${!dir}" || {
echo "Changing group permissions of ${cur_dir_name} directory:"
echo " ${!dir}"
echo "failed."
echo ""
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\`."
} >&2
done
@@ -62,11 +62,11 @@ install_languages() {
# Loop over languages to be installed
for lang in "${langs[@]}"; do
pkg="tesseract-ocr-$lang"
if dpkg -s "$pkg" 2>&1 > /dev/null; then
if dpkg -s "$pkg" > /dev/null 2>&1; then
continue
fi
if ! apt-cache show "$pkg" 2>&1 > /dev/null; then
if ! apt-cache show "$pkg" > /dev/null 2>&1; then
continue
fi

View File

@@ -70,9 +70,14 @@ class DocumentAdmin(CommonAdmin):
created_.short_description = "Created"
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(
"img",
src="/fetch/thumb/{}".format(obj.id),
src=src_link,
width=180,
alt="Thumbnail of {}".format(obj.file_name),
title=obj.file_name

View File

@@ -2,7 +2,6 @@ import magic
import os
from datetime import datetime
from hashlib import sha256
from time import mktime
from django import forms
@@ -14,7 +13,6 @@ from .consumer import Consumer
class UploadForm(forms.Form):
SECRET = settings.SHARED_SECRET
TYPE_LOOKUP = {
"application/pdf": Document.TYPE_PDF,
"image/png": Document.TYPE_PNG,
@@ -32,10 +30,9 @@ class UploadForm(forms.Form):
required=False
)
document = forms.FileField()
signature = forms.CharField(max_length=256)
def __init__(self, *args, **kwargs):
forms.Form.__init__(*args, **kwargs)
forms.Form.__init__(self, *args, **kwargs)
self._file_type = None
def clean_correspondent(self):
@@ -82,17 +79,6 @@ class UploadForm(forms.Form):
return document
def clean(self):
corresp = self.cleaned_data.get("correspondent")
title = self.cleaned_data.get("title")
signature = self.cleaned_data.get("signature")
if sha256(corresp + title + self.SECRET).hexdigest() == signature:
return self.cleaned_data
raise forms.ValidationError("The signature provided did not validate")
def save(self):
"""
Since the consumer already does a lot of work, it's easier just to save
@@ -104,7 +90,7 @@ class UploadForm(forms.Form):
title = self.cleaned_data.get("title")
document = self.cleaned_data.get("document")
t = int(mktime(datetime.now()))
t = int(mktime(datetime.now().timetuple()))
file_name = os.path.join(
Consumer.CONSUME,
"{} - {}.{}".format(correspondent, title, self._file_type)

View File

@@ -43,7 +43,10 @@ class Message(Loggable):
and n attachments, and that we don't care about the message body.
"""
SECRET = settings.SHARED_SECRET
SECRET = os.getenv(
"PAPERLESS_EMAIL_SECRET",
os.getenv("PAPERLESS_SHARED_SECRET") # TODO: Remove after 2017/09
)
def __init__(self, data, group=None):
"""
@@ -153,11 +156,11 @@ class MailFetcher(Loggable):
Loggable.__init__(self)
self._connection = None
self._host = settings.MAIL_CONSUMPTION["HOST"]
self._port = settings.MAIL_CONSUMPTION["PORT"]
self._username = settings.MAIL_CONSUMPTION["USERNAME"]
self._password = settings.MAIL_CONSUMPTION["PASSWORD"]
self._inbox = settings.MAIL_CONSUMPTION["INBOX"]
self._host = os.getenv("PAPERLESS_CONSUME_MAIL_HOST")
self._port = os.getenv("PAPERLESS_CONSUME_MAIL_PORT")
self._username = os.getenv("PAPERLESS_CONSUME_MAIL_USER")
self._password = os.getenv("PAPERLESS_CONSUME_MAIL_PASS")
self._inbox = os.getenv("PAPERLESS_CONSUME_MAIL_INBOX", "INBOX")
self._enabled = bool(self._host)

View File

@@ -38,6 +38,9 @@ class GnuPG(object):
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"))
if set(documents) == {"originals", "thumbnails"}:

View 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'),
),
]

View File

@@ -1,3 +1,5 @@
# coding=utf-8
import dateutil.parser
import logging
import os
@@ -89,7 +91,7 @@ class MatchingModel(models.Model):
search_kwargs = {"flags": re.IGNORECASE}
if self.matching_algorithm == self.MATCH_ALL:
for word in self.match.split(" "):
for word in self._split_match():
search_result = re.search(
r"\b{}\b".format(word), text, **search_kwargs)
if not search_result:
@@ -97,7 +99,7 @@ class MatchingModel(models.Model):
return True
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):
return True
return False
@@ -121,6 +123,21 @@ class MatchingModel(models.Model):
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):
self.match = self.match.lower()
@@ -172,7 +189,12 @@ class Document(models.Model):
TYPES = (TYPE_PDF, TYPE_PNG, TYPE_JPG, TYPE_GIF, TYPE_TIF,)
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)
@@ -316,45 +338,45 @@ class FileInfo(object):
r"(?P<correspondent>.*) - "
r"(?P<title>.*) - "
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
)),
("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<title>.*) - "
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
)),
("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<correspondent>.*) - "
r"(?P<title>.*)"
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
flags=re.IGNORECASE
)),
("created-title", re.compile(
r"^(?P<created>\d\d\d\d\d\d\d\d(\d\d\d\d\d\d)?Z) - "
r"(?P<title>.*)"
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
flags=re.IGNORECASE
)),
("correspondent-title-tags", re.compile(
r"(?P<correspondent>.*) - "
r"(?P<title>.*) - "
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
)),
("correspondent-title", re.compile(
r"(?P<correspondent>.*) - "
r"(?P<title>.*)?"
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
flags=re.IGNORECASE
)),
("title", re.compile(
r"(?P<title>.*)"
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff)$",
r"\.(?P<extension>pdf|jpe?g|png|gif|tiff?)$",
flags=re.IGNORECASE
))
])
@@ -397,6 +419,8 @@ class FileInfo(object):
r = extension.lower()
if r == "jpeg":
return "jpg"
if r == "tif":
return "tiff"
return r
@classmethod

View File

@@ -1,6 +0,0 @@
{% load hacks %}
{# See documents.templatetags.hacks.change_list_results for an explanation #}
{% change_list_results %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -29,18 +29,13 @@
.result .header {
padding: 5px;
background-color: #79AEC8;
height: 6em;
}
.result .header .checkbox {
margin-right: 5px;
}
.result .header .checkbox{
width: 5%;
float: left;
}
.result .header .info {
width: 90%;
float: left;
margin-left: 10%;
}
.result .header a,
.result a.tag {

View File

@@ -6,5 +6,6 @@
<meta charset="utf-8">
</head>
<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>
</html>

View File

@@ -1,41 +1,28 @@
import os
from django.contrib import admin
from django.contrib.admin.templatetags.admin_list import (
result_headers,
result_hidden_fields,
results
)
from django.template import Library
from django.template.loader import get_template
from ..models import Document
register = Library()
@register.simple_tag(takes_context=True)
def change_list_results(context):
@register.inclusion_tag("admin/documents/document/change_list_results.html")
def result_list(cl):
"""
Django has a lot of places where you can override defaults, but
unfortunately, `change_list_results.html` is not one of them. In fact,
it's a downright pain in the ass to override this file on a per-model basis
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.
Copy/pasted from django.contrib.admin.templatetags.admin_list just so I can
modify the value passed to `.inclusion_tag()` in the decorator here. There
must be a cleaner way... right?
"""
path = os.path.join(
os.path.dirname(admin.__file__),
"templates",
"admin",
"change_list_results.html"
)
if context["cl"].model == Document:
path = "admin/documents/document/change_list_results.html"
return get_template(path).render(context)
headers = list(result_headers(cl))
num_sorted_fields = 0
for h in headers:
if h['sortable'] and h['sorted']:
num_sorted_fields += 1
return {'cl': cl,
'result_hidden_fields': list(result_hidden_fields(cl)),
'result_headers': headers,
'num_sorted_fields': num_sorted_fields,
'results': list(results(cl))}

View 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

View File

@@ -58,9 +58,9 @@ class TestAttributes(TestCase):
TAGS = ("tag1", "tag2", "tag3")
EXTENSIONS = (
"pdf", "png", "jpg", "jpeg", "gif",
"PDF", "PNG", "JPG", "JPEG", "GIF",
"PdF", "PnG", "JpG", "JPeG", "GiF",
"pdf", "png", "jpg", "jpeg", "gif", "tiff", "tif",
"PDF", "PNG", "JPG", "JPEG", "GIF", "TIFF", "TIF",
"PdF", "PnG", "JpG", "JPeG", "GiF", "TiFf", "TiF",
)
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)
if extension.lower() == "jpeg":
self.assertEqual(file_info.extension, "jpg", f)
elif extension.lower() == "tif":
self.assertEqual(file_info.extension, "tiff", f)
else:
self.assertEqual(file_info.extension, extension.lower(), f)

View File

@@ -1,6 +1,6 @@
from random import randint
from django.test import TestCase
from django.test import TestCase, override_settings
from ..models import Correspondent, Document, Tag
from ..signals import document_consumption_finished
@@ -16,9 +16,15 @@ class TestMatching(TestCase):
matching_algorithm=getattr(klass, algorithm)
)
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:
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):
@@ -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):
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):
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
doing what we expect wrt to tag & correspondent matching.

View 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)

View File

@@ -1,5 +1,4 @@
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.generic import DetailView, FormView, TemplateView
from django_filters.rest_framework import DjangoFilterBackend
from paperless.db import GnuPG
@@ -81,15 +80,12 @@ class PushView(SessionOrBasicAuthMixin, FormView):
form_class = UploadForm
@classmethod
def as_view(cls, **kwargs):
return csrf_exempt(FormView.as_view(**kwargs))
def form_valid(self, form):
return HttpResponse("1")
form.save()
return HttpResponse("1", status=202)
def form_invalid(self, form):
return HttpResponse("0")
return HttpResponseBadRequest(str(form.errors))
class CorrespondentViewSet(ModelViewSet):

View File

@@ -84,3 +84,20 @@ def binaries_check(app_configs, **kwargs):
check_messages.append(Warning(error.format(binary), hint))
return check_messages
@register()
def config_check(app_configs, **kwargs):
warning = (
"It looks like you have PAPERLESS_SHARED_SECRET defined. Note that "
"in the \npast, this variable was used for both API authentication "
"and as the mail \nkeyword. As the API no no longer uses it, this "
"variable has been renamed to \nPAPERLESS_EMAIL_SECRET, so if you're "
"using the mail feature, you'd best update \nyour variable name.\n\n"
"The old variable will stop working in a few months."
)
if os.getenv("PAPERLESS_SHARED_SECRET"):
return [Warning(warning)]
return []

View File

@@ -47,7 +47,8 @@ _allowed_hosts = os.getenv("PAPERLESS_ALLOWED_HOSTS")
if _allowed_hosts:
ALLOWED_HOSTS = _allowed_hosts.split(",")
FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME")
# Application definition
INSTALLED_APPS = [
@@ -69,6 +70,7 @@ INSTALLED_APPS = [
"rest_framework",
"crispy_forms",
"django_filters"
]
@@ -236,20 +238,6 @@ CONSUMPTION_DIR = os.getenv("PAPERLESS_CONSUMPTION_DIR")
# slowly, you may want to use a higher value than the default.
CONSUMER_LOOP_TIME = int(os.getenv("PAPERLESS_CONSUMER_LOOP_TIME", 10))
# If you want to use IMAP mail consumption, populate this with useful values.
# If you leave HOST set to None, we assume you're not going to use this
# feature.
MAIL_CONSUMPTION = {
"HOST": os.getenv("PAPERLESS_CONSUME_MAIL_HOST"),
"PORT": os.getenv("PAPERLESS_CONSUME_MAIL_PORT"),
"USERNAME": os.getenv("PAPERLESS_CONSUME_MAIL_USER"),
"PASSWORD": os.getenv("PAPERLESS_CONSUME_MAIL_PASS"),
# If True, use SSL/TLS to connect
"USE_SSL": os.getenv("PAPERLESS_CONSUME_MAIL_USE_SSL", "y").lower() == "y",
# The name of the inbox on the server
"INBOX": os.getenv("PAPERLESS_CONSUME_MAIL_INBOX", "INBOX")
}
# This is used to encrypt the original documents and decrypt them later when
# you want to download them. Set it and change the permissions on this file to
# 0600, or set it to `None` and you'll be prompted for the passphrase at
@@ -259,11 +247,6 @@ MAIL_CONSUMPTION = {
# files.
PASSPHRASE = os.getenv("PAPERLESS_PASSPHRASE")
# If you intend to use the "API" to push files into the consumer, you'll need
# to provide a shared secret here. Leaving this as the default will disable
# the API.
SHARED_SECRET = os.getenv("PAPERLESS_SHARED_SECRET", "")
# Trigger a script after every successful document consumption?
PRE_CONSUME_SCRIPT = os.getenv("PAPERLESS_PRE_CONSUME_SCRIPT")
POST_CONSUME_SCRIPT = os.getenv("PAPERLESS_POST_CONSUME_SCRIPT")

View File

@@ -1,28 +1,17 @@
"""paperless URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Add an import: from blog import urls as blog_urls
2. Import the include() function: from django.conf.urls import url, include
3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
"""
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.views.decorators.csrf import csrf_exempt
from django.views.generic import RedirectView
from rest_framework.routers import DefaultRouter
from documents.views import (
IndexView, FetchView, PushView,
CorrespondentViewSet, TagViewSet, DocumentViewSet, LogViewSet
CorrespondentViewSet,
DocumentViewSet,
FetchView,
LogViewSet,
PushView,
TagViewSet
)
from reminders.views import ReminderViewSet
@@ -42,9 +31,6 @@ urlpatterns = [
),
url(r"^api/", include(router.urls, namespace="drf")),
# Normal pages (coming soon)
# url(r"^$", IndexView.as_view(), name="index"),
# File downloads
url(
r"^fetch/(?P<kind>doc|thumb)/(?P<pk>\d+)$",
@@ -52,15 +38,17 @@ urlpatterns = [
name="fetch"
),
# File uploads
url(r"^push$", csrf_exempt(PushView.as_view()), name="push"),
# The Django admin
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)
if settings.SHARED_SECRET:
urlpatterns.insert(0, url(r"^push$", PushView.as_view(), name="push"))
# Text in each page's <h1> (and above login form).
admin.site.site_header = 'Paperless'
# Text at the end of each page's <title>.

View File

@@ -1 +1 @@
__version__ = (0, 5, 0)
__version__ = (1, 0, 0)

View File

@@ -5,7 +5,7 @@ from .parsers import RasterisedDocumentParser
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
def handle(cls, sender, **kwargs):

View File

@@ -12,9 +12,9 @@ class SignalsTestCase(TestCase):
"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",
"pdf", "jpg", "jpeg", "gif", "png", "tiff", "tif", "pnm", "bmp",
"PDF", "JPG", "JPEG", "GIF", "PNG", "TIFF", "TIF", "PNM", "BMP",
"pDf", "jPg", "jpEg", "gIf", "pNg", "tIff", "tIf", "pNm", "bMp",
)
for prefix in prefixes:

View File

@@ -1,3 +1,8 @@
[pytest]
DJANGO_SETTINGS_MODULE=paperless.settings
addopts = --pythonwarnings=all
env =
PAPERLESS_CONSUME=/tmp
PAPERLESS_PASSPHRASE=THISISNOTASECRET
PAPERLESS_SECRET=paperless
PAPERLESS_EMAIL_SECRET=paperless

View File

@@ -5,19 +5,18 @@
[tox]
skipsdist = True
envlist = py34, py35, py36, pep8
envlist = py34, py35, py36, pycodestyle
[testenv]
commands = {envpython} manage.py test
commands = pytest
deps = -r{toxinidir}/../requirements.txt
setenv =
PAPERLESS_CONSUME=/tmp
PAPERLESS_PASSPHRASE=THISISNOTASECRET
PAPERLESS_SECRET=paperless
[testenv:pep8]
commands=pep8
deps=pep8
[testenv:pycodestyle]
commands=pycodestyle
deps=pycodestyle
[pep8]
exclude=.tox,migrations,paperless/settings.py
[pycodestyle]
exclude=
.tox,
migrations,
paperless/settings.py