Compare commits

..

66 Commits
2.1.0 ... 2.3.0

Author SHA1 Message Date
Daniel Quinn
2edf65dd1e Bump to 2.3.0 2018-09-09 21:51:44 +01:00
Daniel Quinn
9a739bdbab Merge pull request #401 from ahyear/patch-1
add migrate commande to docker update process
2018-09-09 21:26:56 +01:00
Daniel Quinn
66db06590d Merge branch 'jat255-ENH_config_inline_or_attach' 2018-09-09 21:22:42 +01:00
Daniel Quinn
7cef108785 Streamline how we handle boolean values in settings.py 2018-09-09 21:22:07 +01:00
Daniel Quinn
a86a20ef0f Make the example file contain the default value 2018-09-09 21:16:53 +01:00
Daniel Quinn
f94347abc0 Merge branch 'ENH_config_inline_or_attach' of git://github.com/jat255/paperless into jat255-ENH_config_inline_or_attach 2018-09-09 21:15:14 +01:00
Daniel Quinn
46cbd10ba0 Merge pull request #399 from jat255/ENH_convert_only_one_page
Speed up thumbnail generation for PDFs
2018-09-09 21:12:42 +01:00
Daniel Quinn
2a96c648e8 Merge pull request #396 from dubit0/postgres_mysql_fix
Fix document checks with PostgreSQL and MySQL backends.
2018-09-09 21:10:36 +01:00
Daniel Quinn
75648cc74b Merge branch 'jat255-ENH_text_consumer' 2018-09-09 21:03:58 +01:00
Daniel Quinn
0472fe4e9e Reorder imports 2018-09-09 21:03:37 +01:00
Daniel Quinn
c99f5923d5 Rename parsers to DATE_REGEX
In moving the `parsers` variable into the package-level, it lost the
context, so a more descriptive name was needed.
2018-09-09 21:02:30 +01:00
Daniel Quinn
ef302abed7 Fix pycodestyle complaints 2018-09-09 20:55:37 +01:00
Daniel Quinn
2dc35cc856 Merge branch 'ENH_text_consumer' of git://github.com/jat255/paperless into jat255-ENH_text_consumer 2018-09-09 20:52:59 +01:00
Daniel Quinn
f4c399f0dd Merge pull request #398 from ddddavidmartin/bump_pyocr_version_for_tesseract_4_support
Bump required version for Pyocr to support the latest tesseract 4.
2018-09-09 20:01:51 +01:00
Daniel Quinn
5342db6ada Fix pycodestyle complaints
Apparently, pycodestyle updated itself to now check for invalid escape
sequences, which only complain if the regex in use isn't a raw string
(r"").
2018-09-09 20:00:12 +01:00
Daniel Quinn
5c39fff51b Add tox to dev dependencies 2018-09-09 19:59:47 +01:00
ahyear
ed0e40d3e6 add migrate commande to docker update process 2018-09-06 15:32:41 +02:00
Joshua Taillon
652ead2f5c remove debugging print statement 2018-09-05 23:05:37 -04:00
Joshua Taillon
be9757894a add INLINE_DOC to settings.py 2018-09-05 23:03:30 -04:00
Joshua Taillon
22378789e2 add option for inline vs. attachment for document rendering 2018-09-05 22:58:38 -04:00
Joshua Taillon
72c828170e move date-matching regex pattern to base parser module for use by all subclasses 2018-09-05 21:13:36 -04:00
Joshua Taillon
cac63494f0 change tesseract parser to only convert first page to save (potentially) massive amounts of work 2018-09-05 15:18:35 -04:00
Daniel Quinn
939a67bd4b Add empty requirements for rtd to reference 2018-09-05 11:16:42 +01:00
Daniel Quinn
fbc6a58f5a Add credits for 2.2.0 that I forgot 2018-09-05 10:59:06 +01:00
Daniel Quinn
01a358d2b0 Re-flow text to keep it <80c wide 2018-09-05 10:58:41 +01:00
David Martin
6b447628ed Bump required version for Pyocr to support the latest tesseract 4.
This recently changed in the official tesseract engine [0]. -psm is
not allowed as an option anymore and --psm has to be used instead. The
latest pyocr enables support for this [1].

[0] tesseract-ocr/tesseract@ee201e1
[1] 5abd0a566a
2018-09-05 13:03:42 +10:00
Thomas Niederprüm
2308d5a613 Catch ProgrammingError in Document checks.
When running PostgreSQL or MariaDB/MySQL backends, a query to a non-existent
table will raise a "ProgrammingError". This patch properly catches this error.
Without this patch all management calls to manage.py will lead to an error when
running PostgreSQL or MariaDB as a backend.
2018-09-04 20:11:48 +02:00
Joshua Taillon
23bf79274c Merge branch 'master' into ENH_text_consumer 2018-09-03 23:47:30 -04:00
Joshua Taillon
4849249d86 explicitly add txt, md, and csv types for consumer and viewer; fix thumbnail generation 2018-09-03 23:46:13 -04:00
Daniel Quinn
ee20af71e8 Bump for 2.2.1 2018-09-03 00:27:40 +01:00
Daniel Quinn
3c8aa3ba42 Don't try to remove SessionAuthenticationMiddleware
It was remove entirely in Django 2.0
2018-09-03 00:25:10 +01:00
Daniel Quinn
778ffa488d Add Tim to the credits for 2.2.0 2018-09-02 21:53:52 +01:00
Daniel Quinn
0868390d63 Merge branch 'dadosch-django-v2' 2018-09-02 21:48:59 +01:00
Daniel Quinn
d5180fe5e1 Updates for 2.2.0 2018-09-02 21:48:09 +01:00
Daniel Quinn
08174a6b52 Add note about the removal of puritanical language 2018-09-02 21:46:52 +01:00
Daniel Quinn
f5e725c691 Switch out field_name= for name=
This appears to be a django-filter version change thing.
2018-09-02 21:26:30 +01:00
Daniel Quinn
2400245b96 pep8 2018-09-02 21:26:20 +01:00
Daniel Quinn
729f005600 Remove old Python 2.x style code 2018-09-02 21:26:06 +01:00
Daniel Quinn
39afe41f08 Drop django-flat-responsive
It's not necessary for Django 2.0+ as the new system is responsive by
default.
2018-09-02 21:25:30 +01:00
Daniel Quinn
2d4008371b Merge @dadosch's changes & fix dependency conflicts 2018-09-02 21:06:40 +01:00
Daniel Quinn
218809ce15 Merge pull request #391 from sbrunner/tag-list
Better interface when we have many tags
2018-09-02 20:57:32 +01:00
Daniel Quinn
7db4410c1b Default sort order for tags to use 'name' 2018-09-02 20:56:45 +01:00
Daniel Quinn
f1e1bb4deb Fix #384: duplicate tags due to case insensitivity 2018-09-02 20:48:51 +01:00
Daniel Quinn
cccc9e1a24 Clean up some linter complaints 2018-09-02 20:33:49 +01:00
Daniel Quinn
39ef81d398 Update dependencies 2018-09-02 20:33:28 +01:00
Joshua Taillon
d6fedbec52 first stab at text consumer 2018-08-30 23:32:41 -04:00
dadosch
ec862ed526 make pycodestyle happy... 2018-08-31 00:17:48 +02:00
dadosch
efb0157337 add fix for messed up html at reminders, thanks to @brookst 2018-08-31 00:05:07 +02:00
dadosch
efc57852d1 remove atomic=False where it is obviously not needed) 2018-08-29 00:37:07 +02:00
dadosch
0b9c4f9963 remove my auto generated migration file 2018-08-29 00:19:08 +02:00
dadosch
633d2b376f PROTECT, not PROTECTION 2018-08-29 00:08:01 +02:00
dadosch
91cecd47af apply some patches from @brookst 2018-08-29 00:04:48 +02:00
Stéphane Brunner
b05fd4870e Better interface when we have many tags 2018-08-26 14:20:07 +02:00
dadosch
40e79a731f builds failing maybe because of old versions 2018-08-24 22:03:26 +02:00
dadosch
6cd06f6c8a improved codestyle, go back to == in requirements 2018-08-24 21:52:27 +02:00
dadosch
b6a870c0e5 django v2 compatible: tests needed 2018-08-24 21:31:43 +02:00
Daniel Quinn
160c256327 Merge pull request #383 from erikarvstedt/tests-nowrite
Stop tests from writing to the source tree
2018-08-17 14:28:10 +01:00
Daniel Quinn
fcd36c8415 Merge pull request #387 from LukaszSolo/master
Enable CORS for localhost
2018-08-17 10:23:40 +01:00
Solo
70608f7e31 Refs feedback - replace multiline logic with single line 2018-08-17 11:48:39 +08:00
Solo
4e5ee24618 Refs Travis - fix for 80 chars limit 2018-08-16 21:48:45 +08:00
Solo
1bb80548d2 Refs feedback:
- fix requirements.txt
- change static CORS regex into configurable tuple list
2018-08-16 21:29:03 +08:00
Solo
96268655d2 Prepare Paperless for new front end
- enable CORS for localhost calls
- add Filter to allow API calls that can select Documents without any tag
2018-08-16 17:05:54 +08:00
Erik Arvstedt
be2cbebaf7 Stop tests from writing to the source tree 2018-07-19 23:48:23 +02:00
Daniel Quinn
c79583dedb Merge branch 'ovv-documentation-gunicorn-systemd' 2018-07-08 22:14:34 +01:00
Daniel Quinn
24d3e7f9d3 Merge @ovv's documentation changes and tweak the grammar a bit 2018-07-08 22:14:17 +01:00
Ovv
6fd9995aa1 Installation documentation setup
Issue #329
2018-04-25 19:51:21 +02:00
37 changed files with 938 additions and 581 deletions

View File

@@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4 to remove puritanical language. The original is available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org [homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/ [version]: http://contributor-covenant.org/version/1/4/

View File

@@ -4,20 +4,20 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
django = "<2.0,>=1.11" django = "<2.1,>=2.0"
pillow = "*" pillow = "*"
coveralls = "*" coveralls = "*"
dateparser = "*" dateparser = "*"
django-cors-headers = "*"
django-crispy-forms = "*" django-crispy-forms = "*"
django-extensions = "*" django-extensions = "*"
django-filter = "*" django-filter = "*"
django-flat-responsive = "*"
djangorestframework = "*" djangorestframework = "*"
factory-boy = "*" factory-boy = "*"
"flake8" = "*"
filemagic = "*" filemagic = "*"
fuzzywuzzy = {extras = ["speedup"], version = "==0.15.0"} fuzzywuzzy = {extras = ["speedup"], version = "==0.15.0"}
gunicorn = "*" gunicorn = "*"
inotify-simple = "*"
langdetect = "*" langdetect = "*"
pdftotext = "*" pdftotext = "*"
pyocr = "*" pyocr = "*"
@@ -35,3 +35,6 @@ pytest-xdist = "*"
[dev-packages] [dev-packages]
ipython = "*" ipython = "*"
sphinx = "*"
tox = "*"

533
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "928fbb4c8952128aef7a2ed2707ce510d31d49df96cfc5f08959698edff6e67f" "sha256": "6d8bad24aa5d0c102b13b5ae27acba04836cd5a07a4003cb2763de1e0a3406b7"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -16,24 +16,33 @@
"default": { "default": {
"apipkg": { "apipkg": {
"hashes": [ "hashes": [
"sha256:2e38399dbe842891fe85392601aab8f40a8f4cc5a9053c326de35a1cc0297ac6", "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6",
"sha256:65d2aa68b28e7d31233bb2ba8eb31cda40e4671f8ac2d6b241e358c9652a74b9" "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"
], ],
"version": "==1.4" "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.5"
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.2.1"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
], ],
"version": "==17.4.0" "version": "==18.2.0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296", "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d" "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
], ],
"version": "==2018.1.18" "version": "==2018.8.24"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -46,11 +55,11 @@
"hashes": [ "hashes": [
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
@@ -70,26 +79,22 @@
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80"
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
], ],
"markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version < '4'",
"version": "==4.5.1" "version": "==4.5.1"
}, },
"coveralls": { "coveralls": {
"hashes": [ "hashes": [
"sha256:32569a43c9dbc13fa8199247580a4ab182ef439f51f65bb7f8316d377a1340e8", "sha256:9dee67e78ec17b36c52b778247762851c8e19a893c9a14e921a2fc37f05fac22",
"sha256:664794748d2e5673e347ec476159a9d87f43e0d2d44950e98ed0e27b98da8346" "sha256:aec5a1f5e34224b9089664a1b62217732381c7de361b6ed1b3c394d7187b352a"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.0" "version": "==1.5.0"
}, },
"dateparser": { "dateparser": {
"hashes": [ "hashes": [
@@ -101,11 +106,19 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:056fe5b9e1f8f7fed9bb392919d64f6b33b3a71cfb0f170a90ee277a6ed32bc2", "sha256:0c5b65847d00845ee404bbc0b4a85686f15eb3001ffddda3db4e9baa265bf136",
"sha256:4d398c7b02761e234bbde490aea13ea94cb539ceeb72805b72303f348682f2eb" "sha256:68aeea369a8130259354b6ba1fa9babe0c5ee6bced505dea4afcd00f765ae38b"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.11.12" "version": "==2.0.8"
},
"django-cors-headers": {
"hashes": [
"sha256:5545009c9b233ea7e70da7dbab7cb1c12afa01279895086f98ec243d7eab46fa",
"sha256:c4c2ee97139d18541a1be7d96fe337d1694623816d83f53cb7c00da9b94acae1"
],
"index": "pypi",
"version": "==2.4.0"
}, },
"django-crispy-forms": { "django-crispy-forms": {
"hashes": [ "hashes": [
@@ -117,26 +130,19 @@
}, },
"django-extensions": { "django-extensions": {
"hashes": [ "hashes": [
"sha256:37a543af370ee3b0721ff50442d33c357dd083e6ea06c5b94a199283b6f9e361", "sha256:1f626353a11479014bfe0d77e76d8f866ebca1bb5d595cb57b776230b9e0eb92",
"sha256:bc9f2946c117bb2f49e5e0633eba783787790ae810ea112fe7fd82fa64de2ff1" "sha256:f21b898598a1628cb73017fb9672e2c5e624133be9764f0eb138e0abf8a62b62"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.6" "version": "==2.1.2"
}, },
"django-filter": { "django-filter": {
"hashes": [ "hashes": [
"sha256:ea204242ea83790e1512c9d0d8255002a652a6f4986e93cee664f28955ba0c22", "sha256:6f4e4bc1a11151178520567b50320e5c32f8edb552139d93ea3e30613b886f56",
"sha256:ec0ef1ba23ef95b1620f5d481334413700fb33f45cd76d56a63f4b0b1d76976a" "sha256:86c3925020c27d072cdae7b828aaa5d165c2032a629abbe3c3a1be1edae61c58"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.1.0" "version": "==2.0.0"
},
"django-flat-responsive": {
"hashes": [
"sha256:451caa2700c541b52fb7ce2d34d3d8dee9e980cf29f5463bc8a8c6256a1a6474"
],
"index": "pypi",
"version": "==2.0"
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
@@ -157,22 +163,24 @@
"sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a", "sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a",
"sha256:fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83" "sha256:fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83"
], ],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.5.0" "version": "==1.5.0"
}, },
"factory-boy": { "factory-boy": {
"hashes": [ "hashes": [
"sha256:bd5a096d0f102d79b6c78cef1c8c0b650f2e1a3ecba351c735c6d2df8dabd29c", "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca",
"sha256:be2abc8092294e4097935a29b4e37f5b9ed3e4205e2e32df215c0315b625995e" "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.10.0" "version": "==2.11.1"
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:226d8fa67a8cf8b4007aab721f67639f130e9cfdc53a7095a2290ebb07a65c71", "sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628",
"sha256:48fed4b4a191e2b42ad20c14115f1c6d36d338b80192075d7573f0f42d7fb321" "sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad"
], ],
"version": "==0.8.13" "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7'",
"version": "==0.9.0"
}, },
"filemagic": { "filemagic": {
"hashes": [ "hashes": [
@@ -181,36 +189,36 @@
"index": "pypi", "index": "pypi",
"version": "==1.6" "version": "==1.6"
}, },
"flake8": {
"hashes": [
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
],
"index": "pypi",
"version": "==3.5.0"
},
"fuzzywuzzy": { "fuzzywuzzy": {
"hashes": [ "hashes": [
"sha256:3759bc6859daa0eecef8c82b45404bdac20c23f23136cf4c18b46b426bbc418f", "sha256:3759bc6859daa0eecef8c82b45404bdac20c23f23136cf4c18b46b426bbc418f",
"sha256:5b36957ccf836e700f4468324fa80ba208990385392e217be077d5cd738ae602" "sha256:5b36957ccf836e700f4468324fa80ba208990385392e217be077d5cd738ae602"
], ],
"index": "pypi", "index": "pypi",
"markers": null,
"version": "==0.15.0" "version": "==0.15.0"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
"sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6", "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
"sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622" "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
], ],
"index": "pypi", "index": "pypi",
"version": "==19.7.1" "version": "==19.9.0"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
], ],
"version": "==2.6" "version": "==2.7"
},
"inotify-simple": {
"hashes": [
"sha256:fc2c10dd73278a1027d0663f2db51240af5946390f363a154361406ebdddd8dd"
],
"index": "pypi",
"version": "==1.1.8"
}, },
"langdetect": { "langdetect": {
"hashes": [ "hashes": [
@@ -219,140 +227,111 @@
"index": "pypi", "index": "pypi",
"version": "==1.0.7" "version": "==1.0.7"
}, },
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea", "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e", "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44" "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
], ],
"version": "==4.1.0" "version": "==4.3.0"
}, },
"pdftotext": { "pdftotext": {
"hashes": [ "hashes": [
"sha256:0b82a9fd255a3f2bf5c861cf9e3174d3c4223e1e441bb060c611dcb4e65c6cb8" "sha256:b7312302007e19fc784263a321b41682f01a582af84e14200cef53b3f4e69a50"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.2" "version": "==2.1.0"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:00633bc2ec40313f4daf351855e506d296ec3c553f21b66720d0f1225ca84c6f", "sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a",
"sha256:03514478db61b034fc5d38b9bf060f994e5916776e93f02e59732a8270069c61", "sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72",
"sha256:040144ba422216aecf7577484865ade90e1a475f867301c48bf9fbd7579efd76", "sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574",
"sha256:16246261ff22368e5e32ad74d5ef40403ab6895171a7fc6d34f6c17cfc0f1943", "sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd",
"sha256:1cb38df69362af35c14d4a50123b63c7ff18ec9a6d4d5da629a6f19d05e16ba8", "sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2",
"sha256:2400e122f7b21d9801798207e424cbe1f716cee7314cd0c8963fdb6fc564b5fb", "sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c",
"sha256:2ee6364b270b56a49e8b8a51488e847ab130adc1220c171bed6818c0d4742455", "sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273",
"sha256:3b4560c3891b05022c464b09121bd507c477505a4e19d703e1027a3a7c68d896", "sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554",
"sha256:41374a6afb3f44794410dab54a0d7175e6209a5a02d407119c81083f1a4c1841", "sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612",
"sha256:438a3faf5f702c8d0f80b9f9f9b8382cfa048ca6a0d64ef71b86b563b0ee0359", "sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934",
"sha256:472a124c640bde4d5468f6991c9fa7e30b723d84ac4195a77c6ab6aea30f2b9c", "sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786",
"sha256:4d32c8e3623a61d6e29ccd024066cd1ba556555abfb4cd714155020e00107e3f", "sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27",
"sha256:4d8077fd649ac40a5c4165f2c22fa2a4ad18c668e271ecb2f9d849d1017a9313", "sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b",
"sha256:62ec7ae98357fcd46002c110bb7cad15fce532776f0cbe7ca1d44c49b837d49d", "sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62",
"sha256:6c7cab6a05351cf61e469937c49dbf3cdf5ffb3eeac71f8d22dc9be3507598d8", "sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9",
"sha256:6eca36905444c4b91fe61f1b9933a47a30480738a1dd26501ff67d94fc2bc112", "sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710",
"sha256:74e2ebfd19c16c28ad43b8a28ff73b904ed382ea4875188838541751986e8c9a", "sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3",
"sha256:7673e7473a13107059377c96c563aa36f73184c29d2926882e0a0210b779a1e7", "sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4",
"sha256:81762cf5fca9a82b53b7b2d0e6b420e0f3b06167b97678c81d00470daa622d58", "sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b",
"sha256:8554bbeb4218d9cfb1917c69e6f2d2ad0be9b18a775d2162547edf992e1f5f1f", "sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f",
"sha256:9b66e968da9c4393f5795285528bc862c7b97b91251f31a08004a3c626d18114", "sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e",
"sha256:a00edb2dec0035e98ac3ec768086f0b06dfabb4ad308592ede364ef573692f55", "sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b",
"sha256:b48401752496757e95304a46213c3155bc911ac884bed2e9b275ce1c1df3e293", "sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a",
"sha256:b6cf18f9e653a8077522bb3aa753a776b117e3e0cc872c25811cfdf1459491c2", "sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f",
"sha256:bb8adab1877e9213385cbb1adc297ed8337e01872c42a30cfaa66ff8c422779c", "sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea",
"sha256:c8a4b39ba380b57a31a4b5449a9d257b1302d8bc4799767e645dcee25725efe1", "sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7",
"sha256:cee9bc75bff455d317b6947081df0824a8f118de2786dc3d74a3503fd631f4ef", "sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4",
"sha256:d0dc1313dff48af64517cbbd85e046d6b477fbe5e9d69712801f024dcb08c62b", "sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333",
"sha256:d5bf527ed83617edd1855a5c923eeeaf68bcb9ac0ceb28e3f19b575b3a424984", "sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109",
"sha256:df5863a21f91de5ecdf7d32a32f406dd9867ebb35d41033b8bd9607a21887599", "sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab"
"sha256:e39142332541ed2884c257495504858b22c078a5d781059b07aba4c3a80d7551",
"sha256:e52e8f675ba0b2b417fa98579e7286a41a8e23871f17f4793772f5aa884fea79",
"sha256:e6dd55d5d94b9e36929325dd0c9ab85bfde84a5fc35947c334c32af1af668944",
"sha256:e87cc1acbebf263f308a8494272c2d42016aa33c32bf14d209c81e1f65e11868",
"sha256:ea0091cd4100519cedfeea2c659f52291f535ac6725e2368bcf59e874f270efa",
"sha256:eeb247f4f4d962942b3b555530b0c63b77473c7bfe475e51c6b75b7344b49ce3",
"sha256:f0d4433adce6075efd24fc0285135248b0b50f5a58129c7e552030e04fe45c7f",
"sha256:f1f3bd92f8e12dc22884935a73c9f94c4d9bd0d34410c456540713d6b7832b8c",
"sha256:f42a87cbf50e905f49f053c0b1fb86c911c730624022bf44c8857244fc4cdaca",
"sha256:f5f302db65e2e0ae96e26670818157640d3ca83a3054c290eff3631598dcf819",
"sha256:f7634d534662bbb08976db801ba27a112aee23e597eeaf09267b4575341e45bf",
"sha256:fdd374c02e8bb2d6468a85be50ea66e1c4ef9e809974c30d8576728473a6ed03",
"sha256:fe6931db24716a0845bd8c8915bd096b77c2a7043e6fc59ae9ca364fe816f08b"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.1.0" "version": "==5.2.0"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:714306e9b9a7b24ee4c1e3ff6463d7f652cdd30f4693121b31572e2fe1fdaea3", "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
], ],
"version": "==0.6.0" "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==0.7.1"
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881", "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a" "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
], ],
"version": "==1.5.3" "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.6.0"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
"sha256:1ec08a51c901dfe44921576ed6e4c1f5b7ecbad403f871397feedb5eb8e4fa14", "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
"sha256:5ff2fbcbab997895ba9ead77e1b38b3ebc2e5c3b8a6194ef918666e4c790a00e", "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.3.1" "version": "==2.4.0"
},
"pyflakes": {
"hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
],
"version": "==1.6.0"
}, },
"pyocr": { "pyocr": {
"hashes": [ "hashes": [
"sha256:9ee8b5f38dd966ca531115fc5fe4715f7fa8961a9f14cd5109c2d938c17a2043" "sha256:b6ba6263fd92da56627dff6d263d991a2246aacd117d1788f11b93f419ca395f"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.5.1" "version": "==0.5.3"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", "sha256:453cbbbe5ce6db38717d282b758b917de84802af4288910c12442984bde7b823",
"sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1" "sha256:a8a07f84e680482eb51e244370aaf2caa6301ef265f37c2bdefb3dd3b663f99d"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.5.0" "version": "==3.8.0"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.5.1" "version": "==2.6.0"
}, },
"pytest-django": { "pytest-django": {
"hashes": [ "hashes": [
"sha256:534505e0261cc566279032d9d887f844235342806fd63a6925689670fa1b29d7", "sha256:2d2e0a618d91c280d463e90bcbea9b4e417609157f611a79685b1c561c4c0836",
"sha256:7501942093db2250a32a4e36826edfc542347bb9b26c78ed0649cdcfd49e5789" "sha256:59683def396923b78d7e191a7086a48193f8d5db869ace79acb38f906522bc7b"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.1" "version": "==3.4.2"
}, },
"pytest-env": { "pytest-env": {
"hashes": [ "hashes": [
@@ -366,6 +345,7 @@
"sha256:e4500cd0509ec4a26535f7d4112a8cc0f17d3a41c29ffd4eab479d2a55b30805", "sha256:e4500cd0509ec4a26535f7d4112a8cc0f17d3a41c29ffd4eab479d2a55b30805",
"sha256:f275cb48a73fc61a6710726348e1da6d68a978f0ec0c54ece5a5fae5977e5a08" "sha256:f275cb48a73fc61a6710726348e1da6d68a978f0ec0c54ece5a5fae5977e5a08"
], ],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==0.2" "version": "==0.2"
}, },
"pytest-sugar": { "pytest-sugar": {
@@ -377,35 +357,35 @@
}, },
"pytest-xdist": { "pytest-xdist": {
"hashes": [ "hashes": [
"sha256:be2662264b035920ba740ed6efb1c816a83c8a22253df7766d129f6a7bfdbd35", "sha256:0875deac20f6d96597036bdf63970887a6f36d28289c2f6682faf652dfea687b",
"sha256:e8f5744acc270b3e7d915bdb4d5f471670f049b6fbd163d4cbd52203b075d30f" "sha256:28e25e79698b2662b648319d3971c0f9ae0e6500f88258ccb9b153c31110ba9b"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.22.2" "version": "==1.23.0"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:3220490fb9741e2342e1cf29a503394fdac874bc39568288717ee67047ff29df", "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:9d8074be4c993fbe4947878ce593052f71dac82932a677d49194d8ce9778002e" "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.7.2" "version": "==2.7.3"
}, },
"python-dotenv": { "python-dotenv": {
"hashes": [ "hashes": [
"sha256:4965ed170bf51c347a89820e8050655e9c25db3837db6602e906b6d850fad85c", "sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77",
"sha256:509736185257111613009974e666568a1b031b028b61b500ef1ab4ee780089d5" "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.8.2" "version": "==0.9.1"
}, },
"python-gnupg": { "python-gnupg": {
"hashes": [ "hashes": [
"sha256:38f18712b7cfdd0d769bc88a21e90138154b9be2cbffb1e7d28bc37ee73a1c47", "sha256:2d158dfc6b54927752b945ebe57e6a0c45da27747fa3b9ae66eccc0d2147ac0d",
"sha256:5a54a6dd25bf78d3758dd7a1864f4efd122f9ca9402101d90e3ec4483ceafb73" "sha256:faa69bab58ed0936f0ccf96c99b92369b7a1819305d37dfe5c927d21a437a09d"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.4.2" "version": "==0.4.3"
}, },
"python-levenshtein": { "python-levenshtein": {
"hashes": [ "hashes": [
@@ -415,38 +395,38 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555", "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749" "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
], ],
"index": "pypi", "index": "pypi",
"version": "==2018.4" "version": "==2018.5"
}, },
"regex": { "regex": {
"hashes": [ "hashes": [
"sha256:1b428a296531ea1642a7da48562746309c5c06471a97bd0c02dd6a82e9cecee8", "sha256:22d7ef8c2df344328a8a3c61edade2ee714e5de9360911d22a9213931c769faa",
"sha256:27d72bb42dffb32516c28d218bb054ce128afd3e18464f30837166346758af67", "sha256:3a699780c6b712c67dc23207b129ccc6a7e1270233f7aadead3ea3f83c893702",
"sha256:32cf4743debee9ea12d3626ee21eae83052763740e04086304e7a74778bf58c9", "sha256:42f460d349baebd5faec02a0c920988fb0300b24baf898d9c139886565b66b6c",
"sha256:32f6408dbca35040bc65f9f4ae1444d5546411fde989cb71443a182dd643305e", "sha256:43bf3d79940cbdf19adda838d8b26b28b47bec793cda46590b5b25703742f440",
"sha256:333687d9a44738c486735955993f83bd22061a416c48f5a5f9e765e90cf1b0c9", "sha256:47d6c7f0588ef33464e00023067c4e7cce68e0d6a686a73c7ee15abfdad503d4",
"sha256:35eeccf17af3b017a54d754e160af597036435c58eceae60f1dd1364ae1250c7", "sha256:5b879f59f25ed9b91bc8693a9a994014b431f224f492519ad0255ce6b54b83e5",
"sha256:361a1fd703a35580a4714ec28d85e29780081a4c399a99bbfb2aee695d72aedb", "sha256:8ba0093c412900f636b0f826c597a0c3ea0e395344bc99894ddefe88b76c9c7e",
"sha256:494bed6396a20d3aa6376bdf2d3fbb1005b8f4339558d8ac7b53256755f80303", "sha256:a4789254a1a0bd7a637036cce0b7ed72d8cc864e93f2e9cfd10ac00ae27bb7b0",
"sha256:5b9c0ddd5b4afa08c9074170a2ea9b34ea296e32aeea522faaaaeeeb2fe0af2e", "sha256:b73cea07117dca888b0c3671770b501bef19aac9c45c8ffdb5bea2cca2377b0a",
"sha256:a50532f61b23d4ab9d216a6214f359dd05c911c1a1ad20986b6738a782926c1a", "sha256:d3eb59fa3e5b5438438ec97acd9dc86f077428e020b015b43987e35bea68ef4c",
"sha256:a9243d7b359b72c681a2c32eaa7ace8d346b7e8ce09d172a683acf6853161d9c", "sha256:d51d232b4e2f106deaf286001f563947fee255bc5bd209a696f027e15cf0a1e7",
"sha256:b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29", "sha256:d59b03131a8e35061b47a8f186324a95eaf30d5f6ee9cc0637e7b87d29c7c9b5",
"sha256:be42a601aaaeb7a317f818490a39d153952a97c40c6e9beeb2a1103616405348", "sha256:dd705df1b47470388fc4630e4df3cbbe7677e2ab80092a1c660cae630a307b2d",
"sha256:eee4d94b1a626490fc8170ffd788883f8c641b576e11ba9b4a29c9f6623371e0", "sha256:e87fffa437a4b00afb17af785da9b01618425d6cd984c677639deb937037d8f2",
"sha256:f69d1201a4750f763971ea8364ed95ee888fc128968b39d38883a72a4d005895" "sha256:ed40e0474ab5ab228a8d133759d451b31d3ccdebaff698646e54aff82c3de4f8"
], ],
"version": "==2018.2.21" "version": "==2018.8.29"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
], ],
"version": "==2.18.4" "version": "==2.19.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@@ -476,13 +456,28 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
], ],
"version": "==1.22" "markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version < '4' and python_version != '3.3.*'",
"version": "==1.23"
} }
}, },
"develop": { "develop": {
"alabaster": {
"hashes": [
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
],
"version": "==0.7.11"
},
"babel": {
"hashes": [
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
],
"version": "==2.6.0"
},
"backcall": { "backcall": {
"hashes": [ "hashes": [
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
@@ -490,6 +485,20 @@
], ],
"version": "==0.1.0" "version": "==0.1.0"
}, },
"certifi": {
"hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
"version": "==2018.8.24"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"decorator": { "decorator": {
"hashes": [ "hashes": [
"sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
@@ -497,13 +506,36 @@
], ],
"version": "==4.3.0" "version": "==4.3.0"
}, },
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
],
"version": "==0.14"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"imagesize": {
"hashes": [
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.1.0"
},
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:85882f97d75122ff8cdfe129215a408085a26039527110c8d4a2b8a5e45b7639", "sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62",
"sha256:a6ac981381b3f5f604b37a293369963485200e3639fb0404fa76092383c10c41" "sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.3.1" "version": "==6.5.0"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@@ -514,25 +546,45 @@
}, },
"jedi": { "jedi": {
"hashes": [ "hashes": [
"sha256:1972f694c6bc66a2fac8718299e2ab73011d653a6d8059790c3476d2353b99ad", "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1",
"sha256:5861f6dc0c16e024cbb0044999f9cf8013b292c05f287df06d3d991a87a4eb89" "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f"
], ],
"version": "==0.12.0" "version": "==0.12.1"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"packaging": {
"hashes": [
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
],
"version": "==17.1"
}, },
"parso": { "parso": {
"hashes": [ "hashes": [
"sha256:62bd6bf7f04ab5c817704ff513ef175328676471bdef3629d4bdd46626f75551", "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
"sha256:a75a304d7090d2c67bd298091c14ef9d3d560e3c53de1c239617889f61d1d307" "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
], ],
"version": "==0.2.0" "version": "==0.3.1"
}, },
"pexpect": { "pexpect": {
"hashes": [ "hashes": [
"sha256:9783f4644a3ef8528a6f20374eeb434431a650c797ca6d8df0d81e30fffdfa24", "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba",
"sha256:9f8eb3277716a01faafaba553d629d3d60a1a624c7cf45daa600d2148c30020c" "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"
], ],
"markers": "sys_platform != 'win32'", "markers": "sys_platform != 'win32'",
"version": "==4.5.0" "version": "==4.6.0"
}, },
"pickleshare": { "pickleshare": {
"hashes": [ "hashes": [
@@ -541,6 +593,14 @@
], ],
"version": "==0.7.4" "version": "==0.7.4"
}, },
"pluggy": {
"hashes": [
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==0.7.1"
},
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381",
@@ -551,10 +611,18 @@
}, },
"ptyprocess": { "ptyprocess": {
"hashes": [ "hashes": [
"sha256:e64193f0047ad603b71f202332ab5527c5e52aa7c8b609704fc28c0dc20c4365", "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
"sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a" "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
], ],
"version": "==0.5.2" "version": "==0.6.0"
},
"py": {
"hashes": [
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.6.0"
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
@@ -563,6 +631,28 @@
], ],
"version": "==2.2.0" "version": "==2.2.0"
}, },
"pyparsing": {
"hashes": [
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
],
"version": "==2.2.0"
},
"pytz": {
"hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
],
"index": "pypi",
"version": "==2018.5"
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"version": "==2.19.1"
},
"simplegeneric": { "simplegeneric": {
"hashes": [ "hashes": [
"sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"
@@ -576,6 +666,37 @@
], ],
"version": "==1.11.0" "version": "==1.11.0"
}, },
"snowballstemmer": {
"hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
],
"version": "==1.2.1"
},
"sphinx": {
"hashes": [
"sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
"sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
],
"index": "pypi",
"version": "==1.7.9"
},
"sphinxcontrib-websupport": {
"hashes": [
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
],
"markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*'",
"version": "==1.1.0"
},
"tox": {
"hashes": [
"sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7",
"sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600"
],
"index": "pypi",
"version": "==3.2.1"
},
"traitlets": { "traitlets": {
"hashes": [ "hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
@@ -583,6 +704,22 @@
], ],
"version": "==4.3.2" "version": "==4.3.2"
}, },
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version < '4' and python_version != '3.3.*'",
"version": "==1.23"
},
"virtualenv": {
"hashes": [
"sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
"sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
],
"markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'",
"version": "==16.0.0"
},
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",

View File

@@ -1,6 +1,47 @@
Changelog Changelog
######### #########
2.3.0
=====
* Support for consuming plain text & markdown documents was added by
`Joshua Taillon`_! This was a long-requested feature, and it's addition is
likely to be greatly appreciated by the community: `#395`_ Thanks also to
`David Martin`_ for his assistance on the issue.
* `dubit0`_ found & fixed a bug that prevented management commands from running
before we had an operational database: `#396`_
* Joshua also added a simple update to the thumbnail generation process to
improve performance: `#399`_
* As his last bit of effort on this release, Joshua also added some code to
allow you to view the documents inline rather than download them as an
attachment. `#400`_
* Finally, `ahyear`_ found a slip in the Docker documentation and patched it. `#401`_
2.2.1
=====
* `Kyle Lucy`_ reported a bug quickly after the release of 2.2.0 where we broke
the ``DISABLE_LOGIN`` feature: `#392`_.
2.2.0
=====
* Thanks to `dadosch`_, `Wolfgang Mader`_, and `Tim Brooks`_ this is the first
version of Paperless that supports Django 2.0! As a result of their hard
work, you can now also run Paperless on Python 3.7 as well: `#386`_ &
`#390`_.
* `Stéphane Brunner`_ added a few lines of code that made tagging interface a lot
easier on those of us with lots of different tags: `#391`_.
* `Kilian Koeltzsch`_ noticed a bug in how we capture & automatically create
tags, so that's fixed now too: `#384`_.
* `erikarvstedt`_ tweaked the behaviour of the test suite to be better behaved
for packaging environments: `#383`_.
* `Lukasz Soluch`_ added CORS support to make building a new Javascript-based front-end
cleaner & easier: `#387`_.
2.1.0 2.1.0
===== =====
@@ -451,6 +492,15 @@ bulk of the work on this big change.
.. _mcronce: https://github.com/mcronce .. _mcronce: https://github.com/mcronce
.. _Enno Lohmeier: https://github.com/elohmeier .. _Enno Lohmeier: https://github.com/elohmeier
.. _Mark McFate: https://github.com/SummittDweller .. _Mark McFate: https://github.com/SummittDweller
.. _dadosch: https://github.com/dadosch
.. _Wolfgang Mader: https://github.com/wmader
.. _Tim Brooks: https://github.com/brookst
.. _Stéphane Brunner: https://github.com/sbrunner
.. _Kilian Koeltzsch: https://github.com/kiliankoe
.. _Lukasz Soluch: https://github.com/LukaszSolo
.. _Joshua Taillon: https://github.com/jat255
.. _dubit0: https://github.com/dubit0
.. _ahyear: https://github.com/ahyear
.. _#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
@@ -525,6 +575,18 @@ bulk of the work on this big change.
.. _#374: https://github.com/danielquinn/paperless/pull/374 .. _#374: https://github.com/danielquinn/paperless/pull/374
.. _#375: https://github.com/danielquinn/paperless/pull/375 .. _#375: https://github.com/danielquinn/paperless/pull/375
.. _#376: https://github.com/danielquinn/paperless/pull/376 .. _#376: https://github.com/danielquinn/paperless/pull/376
.. _#383: https://github.com/danielquinn/paperless/pull/383
.. _#384: https://github.com/danielquinn/paperless/issues/384
.. _#386: https://github.com/danielquinn/paperless/issues/386
.. _#387: https://github.com/danielquinn/paperless/pull/387
.. _#391: https://github.com/danielquinn/paperless/pull/391
.. _#390: https://github.com/danielquinn/paperless/pull/390
.. _#392: https://github.com/danielquinn/paperless/issues/392
.. _#395: https://github.com/danielquinn/paperless/pull/395
.. _#396: https://github.com/danielquinn/paperless/pull/396
.. _#399: https://github.com/danielquinn/paperless/pull/399
.. _#400: https://github.com/danielquinn/paperless/pull/400
.. _#401: https://github.com/danielquinn/paperless/pull/401
.. _pipenv: https://docs.pipenv.org/ .. _pipenv: https://docs.pipenv.org/
.. _a new home on Docker Hub: https://hub.docker.com/r/danielquinn/paperless/ .. _a new home on Docker Hub: https://hub.docker.com/r/danielquinn/paperless/

View File

@@ -101,6 +101,7 @@ is similar:
$ cd /path/to/project $ cd /path/to/project
$ git pull $ git pull
$ docker build -t paperless . $ docker build -t paperless .
$ docker-compose run --rm comsumer migrate
$ docker-compose up -d $ docker-compose up -d
If ``git pull`` doesn't report any changes, there is no need to continue with If ``git pull`` doesn't report any changes, there is no need to continue with

0
docs/requirements.txt Normal file
View File

View File

@@ -39,33 +39,38 @@ or just download the tarball and go that route:
Installation & Configuration Installation & Configuration
---------------------------- ----------------------------
You can go multiple routes with setting up and running Paperless. The `Vagrant You can go multiple routes with setting up and running Paperless:
route`_ is quick & easy, but means you're running a VM which comes with memory
consumption etc. We also `support Docker`_, which you can use natively under * The `bare metal route`_
Linux and in a VM with `Docker Machine`_ (this guide was written for native * The `vagrant route`_
Docker usage under Linux, you might have to adapt it for Docker Machine.) * The `docker route`_
Not to forget the virtualenv, this is similar to `bare metal`_ with the
exception that you have to activate the virtualenv first.
Last but not least, the standard `bare metal`_ approach is a little more The `Vagrant route`_ is quick & easy, but means you're running a VM which comes
complicated, but worth it because it makes it easier should you want to with memory consumption, cpu overhead etc. The `docker route`_ offers the same
contribute some code back. simplicity as Vagrant with lower resource consumption.
The `bare metal route`_ is a bit more complicated to setup but makes it easier
should you want to contribute some code back.
.. _Vagrant route: setup-installation-vagrant_ .. _Vagrant route: setup-installation-vagrant_
.. _support Docker: setup-installation-docker_ .. _docker route: setup-installation-docker_
.. _bare metal: setup-installation-standard_ .. _bare metal route: setup-installation-bare-metal_
.. _Docker Machine: https://docs.docker.com/machine/ .. _Docker Machine: https://docs.docker.com/machine/
.. _setup-installation-standard: .. _setup-installation-bare-metal:
Standard (Bare Metal) Standard (Bare Metal)
..................... +++++++++++++++++++++
1. Install the requirements as per the :ref:`requirements <requirements>` page. 1. Install the requirements as per the :ref:`requirements <requirements>` page.
2. Within the extract of master.zip go to the ``src`` directory. 2. Within the extract of master.zip go to the ``src`` directory.
3. Copy ``../paperless.conf.example`` to ``/etc/paperless.conf`` and open it in 3. Copy ``../paperless.conf.example`` to ``/etc/paperless.conf`` and open it in
your favourite editor. Because this file contains passwords it should only your favourite editor. As this file contains passwords. It should only be
be readable by user root and paperless! Set the values for: readable by user root and paperless! Set the values for:
Set the values for:
* ``PAPERLESS_CONSUMPTION_DIR``: this is where your documents will be * ``PAPERLESS_CONSUMPTION_DIR``: this is where your documents will be
dumped to be consumed by Paperless. dumped to be consumed by Paperless.
@@ -82,9 +87,10 @@ Standard (Bare Metal)
6. Start the webserver with ``./manage.py runserver <IP>:<PORT>``. 6. Start the webserver with ``./manage.py runserver <IP>:<PORT>``.
If no specifc IP or port are given, the default is ``127.0.0.1:8000`` If no specifc IP or port are given, the default is ``127.0.0.1:8000``
also known as http://localhost:8000/. also known as http://localhost:8000/.
You should now be able to visit your (empty) at `Paperless webserver`_ or You should now be able to visit your (empty) installation at
whatever you chose before. You can login with the user/pass you created in `Paperless webserver`_ or whatever you chose before. You can login with the
#5. user/pass you created in #5.
7. In a separate window, change to the ``src`` directory in this repo again, 7. In a separate window, change to the ``src`` directory in this repo again,
but this time, you should start the consumer script with but this time, you should start the consumer script with
``./manage.py document_consumer``. ``./manage.py document_consumer``.
@@ -93,13 +99,18 @@ Standard (Bare Metal)
10. Visit the document list on your webserver, and it should be there, indexed 10. Visit the document list on your webserver, and it should be there, indexed
and downloadable. and downloadable.
.. _Paperless webserver: http://127.0.0.1:8000 .. caution::
This installation is not secure. Once everything is working head over to
`Making things more permanent`_
.. _Paperless webserver: http://127.0.0.1:8000
.. _Making things more permanent: setup-permanent_
.. _setup-installation-docker: .. _setup-installation-docker:
Docker Method Docker Method
............. +++++++++++++
1. Install `Docker`_. 1. Install `Docker`_.
@@ -259,7 +270,7 @@ Docker Method
.. _setup-installation-vagrant: .. _setup-installation-vagrant:
Vagrant Method Vagrant Method
.............. ++++++++++++++
1. Install `Vagrant`_. How you do that is really between you and your OS. 1. Install `Vagrant`_. How you do that is really between you and your OS.
2. Run ``vagrant up``. An instance will start up for you. When it's ready and 2. Run ``vagrant up``. An instance will start up for you. When it's ready and
@@ -295,6 +306,11 @@ Vagrant Method
11. Visit the document list on your webserver, and it should be there, indexed 11. Visit the document list on your webserver, and it should be there, indexed
and downloadable. and downloadable.
.. caution::
This installation is not secure. Once everything is working head up to
`Making things more permanent`_
.. _Vagrant: https://vagrantup.com/ .. _Vagrant: https://vagrantup.com/
.. _Paperless server: http://172.28.128.4:8000 .. _Paperless server: http://172.28.128.4:8000
@@ -304,116 +320,39 @@ Vagrant Method
Making Things a Little more Permanent Making Things a Little more Permanent
------------------------------------- -------------------------------------
Once you've tested things and are happy with the work flow, you can automate Once you've tested things and are happy with the work flow, you should secure
the process of starting the webserver and consumer automatically. the installation and automate the process of starting the webserver and
consumer.
.. _setup-permanent-standard-systemd: .. _setup-permanent-webserver:
Standard (Bare Metal, Systemd)
..............................
If you're running on a bare metal system that's using Systemd, you can use the
service unit files in the ``scripts`` directory to set this up. You'll need to
create a user called ``paperless`` (without login (if not already done so #5))
and setup Paperless to be in a place that this new user can read and write to.
Be sure to edit the service scripts to point to the proper location of your
paperless install, referencing the appropriate Python binary. For example:
``ExecStart=/path/to/python3 /path/to/paperless/src/manage.py document_consumer``.
If you don't want to make a new user, you can change the ``Group`` and ``User``
variables accordingly.
Then, as ``root`` (or using ``sudo``) you can just copy the ``.service`` files
to the Systemd directory and tell it to enable the two services::
# cp /path/to/paperless/scripts/paperless-consumer.service /etc/systemd/system/
# cp /path/to/paperless/scripts/paperless-webserver.service /etc/systemd/system/
# systemctl enable paperless-consumer
# systemctl enable paperless-webserver
# systemctl start paperless-consumer
# systemctl start paperless-webserver
.. _setup-permanent-standard-ubuntu14:
Ubuntu 14.04 (Bare Metal, Upstart)
..................................
Ubuntu 14.04 and earlier use the `Upstart`_ init system to start services
during the boot process. To configure Upstart to run Paperless automatically
after restarting your system:
1. Change to the directory where Upstart's configuration files are kept:
``cd /etc/init``
2. Create a new file: ``sudo nano paperless-server.conf``
3. In the newly-created file enter::
start on (local-filesystems and net-device-up IFACE=eth0)
stop on shutdown
respawn
respawn limit 10 5
script
exec /srv/paperless/src/manage.py runserver --noreload 0.0.0.0:80
end script
Note that you'll need to replace ``/srv/paperless/src/manage.py`` with the
path to the ``manage.py`` script in your installation directory.
If you are using a network interface other than ``eth0``, you will have to
change ``IFACE=eth0``. For example, if you are connected via WiFi, you will
likely need to replace ``eth0`` above with ``wlan0``. To see all interfaces,
run ``ifconfig -a``.
Save the file.
4. Create a new file: ``sudo nano paperless-consumer.conf``
5. In the newly-created file enter::
start on (local-filesystems and net-device-up IFACE=eth0)
stop on shutdown
respawn
respawn limit 10 5
script
exec /srv/paperless/src/manage.py document_consumer
end script
Replace ``/srv/paperless/src/manage.py`` with the same values as in step 3
above and replace ``eth0`` with the appropriate value, if necessary. Save the
file.
These two configuration files together will start both the Paperless webserver
and document consumer processes when the file system and network interface
specified is available after boot. Furthermore, if either process ever exits
unexpectedly, Upstart will try to restart it a maximum of 10 times within a 5
second period.
.. _Upstart: http://upstart.ubuntu.com/
.. _setup-permanent-vagrant:
Using a Real Webserver 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 it is heavily discouraged to use
it's probably a good idea to use a webserver capable of handling more than one it for more than that.
thread. You will also have to let the webserver serve the static files (CSS,
JavaScript) from the directory configured in ``PAPERLESS_STATICDIR``. For that, If you want to do things right you should use a real webserver capable of
you need to run ``./manage.py collectstatic`` in the ``src`` directory. The handling more than one thread. You will also have to let the webserver serve
default static files directory is ``../static``. the static files (CSS, JavaScript) from the directory configured in
``PAPERLESS_STATICDIR``. The default static files directory is ``../static``.
For that you need to activate your virtual environment and collect the static
files with the command:
.. code:: bash
$ cd <paperless directory>/src
$ ./manage.py collectstatic
Apache Apache
~~~~~~ ~~~~~~
This is a configuration supplied by `steckerhalter`_ on GitHub. It uses Apache This is a configuration supplied by `steckerhalter`_ on GitHub. It uses Apache
and mod_wsgi, with a Paperless installation in /home/paperless/: and mod_wsgi, with a Paperless installation in ``/home/paperless/``:
.. code:: apache .. code:: apache
@@ -444,170 +383,150 @@ Nginx + Gunicorn
If you're using Nginx, the most common setup is to combine it with a If you're using Nginx, the most common setup is to combine it with a
Python-based server like Gunicorn so that Nginx is acting as a proxy. Below is Python-based server like Gunicorn so that Nginx is acting as a proxy. Below is
a copy of a simple Nginx configuration fragment making use of SSL and IPv6 to a copy of a simple Nginx configuration fragment making use of a gunicorn
refer to a gunicorn instance listening on a local Unix socket: instance listening on localhost port 8000.
.. code:: nginx .. code:: nginx
upstream transfer_server {
server unix:/run/example.com/gunicorn.sock fail_timeout=0;
}
# Redirect requests on port 80 to 443
server { server {
listen 80; listen 80;
listen [::]:80;
server_name example.com; index index.html index.htm index.php;
rewrite ^ https://$server_name$request_uri? permanent; access_log /var/log/nginx/paperless_access.log;
error_log /var/log/nginx/paperless_error.log;
location /static {
autoindex on;
alias <path-to-paperless-static-directory>
}
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000
}
} }
server {
listen 443 ssl; The gunicorn server can be started with the command:
listen [::]:443;
client_max_body_size 4G;
server_name example.com;
keepalive_timeout 5;
root /var/www/example.com;
ssl on; .. code-block:: shell
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; $ <path-to-paperless-virtual-environment>/bin/gunicorn <path-to-paperless>/src/paperless.wsgi -w 2
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
# Generate with:
# openssl dhparam -out /etc/nginx/dhparam.pem 2048
ssl_dhparam /etc/nginx/dhparam.pem;
# What Mozilla calls "Intermediate configuration" .. _setup-permanent-standard-systemd:
# Copied from https://mozilla.github.io/server-side-tls/ssl-config-generator/
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security max-age=15768000; Standard (Bare Metal + Systemd)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ssl_stapling on; If you're running on a bare metal system that's using Systemd, you can use the
ssl_stapling_verify on; service unit files in the ``scripts`` directory to set this up.
access_log /var/log/nginx/example.com.log main; 1. You'll need to create a group and user called ``paperless`` (without login)
error_log /var/log/nginx/example.com.err info; 2. Setup Paperless to be in a place that this new user can read and write to.
3. Ensure ``/etc/paperless`` is readable by the ``paperless`` user.
4. Copy the service file from the ``scripts`` directory to
``/etc/systemd/system``.
location / { .. code-block:: bash
try_files $uri @proxy_to_app;
}
location @proxy_to_app { $ cp /path/to/paperless/scripts/paperless-consumer.service /etc/systemd/system/
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; $ cp /path/to/paperless/scripts/paperless-webserver.service /etc/systemd/system/
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
proxy_redirect off;
proxy_pass http://transfer_server;
}
} 5. Edit the service file to point the ``ExecStart`` line to the proper location
of your paperless install, referencing the appropriate Python binary. For
example:
``ExecStart=/path/to/python3 /path/to/paperless/src/manage.py document_consumer``.
6. Start and enable (so they start on boot) the services.
Once you've got Nginx configured, you'll want to have a configuration file for .. code-block:: bash
your gunicorn instance. This should do the trick:
.. code:: python $ systemctl enable paperless-consumer
$ systemctl enable paperless-webserver
$ systemctl start paperless-consumer
$ systemctl start paperless-webserver
import os
bind = 'unix:/run/example.com/gunicorn.sock' .. _setup-permanent-standard-upstart:
backlog = 2048
workers = 6
worker_class = 'sync'
worker_connections = 1000
timeout = 30
keepalive = 2
debug = False
spew = False
daemon = False
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
errorlog = '/var/log/example.com/gunicorn.err'
loglevel = 'warning'
accesslog = '/var/log/example.com/gunicorn.log'
proc_name = None
def post_fork(server, worker): Standard (Bare Metal + Upstart)
server.log.info("Worker spawned (pid: %s)", worker.pid) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def pre_fork(server, worker): Ubuntu 14.04 and earlier use the `Upstart`_ init system to start services
pass during the boot process. To configure Upstart to run Paperless automatically
after restarting your system:
def pre_exec(server): 1. Change to the directory where Upstart's configuration files are kept:
server.log.info("Forked child, re-executing.") ``cd /etc/init``
2. Create a new file: ``sudo nano paperless-server.conf``
3. In the newly-created file enter::
def when_ready(server): start on (local-filesystems and net-device-up IFACE=eth0)
server.log.info("Server is ready. Spawning workers") stop on shutdown
def worker_int(worker): respawn
worker.log.info("worker received INT or QUIT signal") respawn limit 10 5
## get traceback info script
import threading, sys, traceback exec <path to paperless virtual environment>/bin/gunicorn <path to parperless>/src/paperless.wsgi -w 2
id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) end script
code = []
for threadId, stack in sys._current_frames().items(): Note that you'll need to replace ``/srv/paperless/src/manage.py`` with the
code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), path to the ``manage.py`` script in your installation directory.
threadId))
for filename, lineno, name, line in traceback.extract_stack(stack): If you are using a network interface other than ``eth0``, you will have to
code.append('File: "%s", line %d, in %s' % (filename, change ``IFACE=eth0``. For example, if you are connected via WiFi, you will
lineno, name)) likely need to replace ``eth0`` above with ``wlan0``. To see all interfaces,
if line: run ``ifconfig -a``.
code.append(" %s" % (line.strip()))
worker.log.debug("\n".join(code)) Save the file.
4. Create a new file: ``sudo nano paperless-consumer.conf``
5. In the newly-created file enter::
start on (local-filesystems and net-device-up IFACE=eth0)
stop on shutdown
respawn
respawn limit 10 5
script
exec <path to paperless virtual environment>/bin/python <path to parperless>/manage.py document_consumer
end script
Replace the path placeholder and ``eth0`` with the appropriate value and save the file.
These two configuration files together will start both the Paperless webserver
and document consumer processes when the file system and network interface
specified is available after boot. Furthermore, if either process ever exits
unexpectedly, Upstart will try to restart it a maximum of 10 times within a 5
second period.
.. _Upstart: http://upstart.ubuntu.com/
def worker_abort(worker):
worker.log.info("worker received SIGABRT signal")
Vagrant Vagrant
....... ~~~~~~~
You may use the Ubuntu explanation above. Replace You may use the Ubuntu explanation above. Replace
``(local-filesystems and net-device-up IFACE=eth0)`` with ``vagrant-mounted``. ``(local-filesystems and net-device-up IFACE=eth0)`` with ``vagrant-mounted``.
.. _setup-permanent-docker: .. _setup-permanent-docker:
Docker Docker
...... ~~~~~~
If you're using Docker, you can set a restart-policy_ in the If you're using Docker, you can set a restart-policy_ in the
``docker-compose.yml`` to have the containers automatically start with the ``docker-compose.yml`` to have the containers automatically start with 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

View File

@@ -89,6 +89,12 @@ PAPERLESS_EMAIL_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"
# If you decide to use the Paperless API in an ajax call, you need to add your
# servers to the list of allowed hosts that can do CORS calls. By default
# Paperless allows calls from localhost:8080, but you'd like to change that,
# you can set this value to a comma-separated list.
#PAPERLESS_CORS_ALLOWED_HOSTS="localhost:8080,example.com,localhost:8000"
# To host paperless under a subpath url like example.com/paperless you set # To host paperless under a subpath url like example.com/paperless you set
# this value to /paperless. No trailing slash! # this value to /paperless. No trailing slash!
# #
@@ -111,6 +117,10 @@ PAPERLESS_EMAIL_SECRET=""
# http://paperless.readthedocs.org/en/latest/consumption.html#hooking-into-the-consumption-process # http://paperless.readthedocs.org/en/latest/consumption.html#hooking-into-the-consumption-process
#PAPERLESS_POST_CONSUME_SCRIPT="/path/to/an/arbitrary/script.sh" #PAPERLESS_POST_CONSUME_SCRIPT="/path/to/an/arbitrary/script.sh"
# By default, when clicking on a document within the web interface, the
# browser will prompt the user to save the document to disk. By setting this to
# "true", the document will instead be opened in the browser, if possible.
#PAPERLESS_INLINE_DOC="false"
# #
# The following values use sensible defaults for modern systems, but if you're # The following values use sensible defaults for modern systems, but if you're

View File

@@ -1,52 +1,51 @@
apipkg==1.4 -i https://pypi.python.org/simple
attrs==18.1.0 apipkg==1.5; python_version != '3.1.*'
certifi==2018.4.16 atomicwrites==1.2.1; python_version != '3.1.*'
attrs==18.2.0
certifi==2018.8.24
chardet==3.0.4 chardet==3.0.4
coverage==4.5.1 coverage==4.5.1; python_version != '3.1.*'
coveralls==1.3.0 coveralls==1.5.0
dateparser==0.7.0 dateparser==0.7.0
django-cors-headers==2.4.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-extensions==2.0.7 django-extensions==2.1.2
django-filter==1.1.0 django-filter==2.0.0
django-flat-responsive==2.0 django==2.0.8
django==1.11.13
djangorestframework==3.8.2 djangorestframework==3.8.2
docopt==0.6.2 docopt==0.6.2
execnet==1.5.0 execnet==1.5.0; python_version != '3.1.*'
factory-boy==2.11.1 factory-boy==2.11.1
faker==0.8.15 faker==0.9.0
filemagic==1.6 filemagic==1.6
flake8==3.5.0
fuzzywuzzy==0.15.0 fuzzywuzzy==0.15.0
gunicorn==19.8.1 gunicorn==19.9.0
idna==2.6 idna==2.7
inotify_simple==1.1.7; sys_platform == 'linux' inotify-simple==1.1.8
langdetect==1.0.7 langdetect==1.0.7
mccabe==0.6.1 more-itertools==4.3.0
more-itertools==4.1.0 pdftotext==2.1.0
pdftotext==2.0.2 pillow==5.2.0
pillow==5.1.0 pluggy==0.7.1; python_version != '3.1.*'
pluggy==0.6.0 py==1.6.0; python_version != '3.1.*'
py==1.5.3 pycodestyle==2.4.0
pycodestyle==2.3.1 pyocr==0.5.3
pyflakes==1.6.0
pyocr==0.5.1
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-django==3.2.1 pytest-django==3.4.2
pytest-env==0.6.2 pytest-env==0.6.2
pytest-forked==0.2 pytest-forked==0.2
pytest-sugar==0.9.1 pytest-sugar==0.9.1
pytest-xdist==1.22.2 pytest-xdist==1.23.0
pytest==3.5.1 pytest==3.7.4
python-dateutil==2.7.3 python-dateutil==2.7.3
python-dotenv==0.8.2 python-dotenv==0.9.1
python-gnupg==0.4.2 python-gnupg==0.4.3
python-levenshtein==0.12.0 python-levenshtein==0.12.0
pytz==2018.4 pytz==2018.5
regex==2018.2.21 regex==2018.8.29
requests==2.18.4 requests==2.19.1
six==1.11.0 six==1.11.0
termcolor==1.1.0 termcolor==1.1.0
text-unidecode==1.2 text-unidecode==1.2
tzlocal==1.5.1 tzlocal==1.5.1
urllib3==1.22 urllib3==1.23; python_version != '3.0.*'

View File

@@ -4,7 +4,7 @@ Description=Paperless webserver
[Service] [Service]
User=paperless User=paperless
Group=paperless Group=paperless
ExecStart=/home/paperless/project/virtualenv/bin/python /home/paperless/project/src/manage.py runserver --noreload 0.0.0.0:8000 ExecStart=/home/paperless/project/virtualenv/bin/gunicorn /home/paperless/project/src/paperless.wsgi -w 2
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -3,8 +3,13 @@ from datetime import datetime
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.urlresolvers import reverse try:
from django.core.urlresolvers import reverse
except ImportError:
from django.urls import reverse
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.safestring import mark_safe
from django.utils.html import format_html, format_html_join
from .models import Correspondent, Tag, Document, Log from .models import Correspondent, Tag, Document, Log
@@ -137,6 +142,7 @@ class DocumentAdmin(CommonAdmin):
"tags_") "tags_")
list_filter = ("tags", "correspondent", FinancialYearFilter, list_filter = ("tags", "correspondent", FinancialYearFilter,
MonthListFilter) MonthListFilter)
filter_horizontal = ("tags",)
ordering = ["-created", "correspondent"] ordering = ["-created", "correspondent"]
@@ -177,7 +183,7 @@ class DocumentAdmin(CommonAdmin):
) )
} }
) )
return r return mark_safe(r)
tags_.allow_tags = True tags_.allow_tags = True
def document(self, obj): def document(self, obj):
@@ -197,16 +203,13 @@ class DocumentAdmin(CommonAdmin):
@staticmethod @staticmethod
def _html_tag(kind, inside=None, **kwargs): def _html_tag(kind, inside=None, **kwargs):
attributes = format_html_join(' ', '{}="{}"', kwargs.items())
attributes = []
for lft, rgt in kwargs.items():
attributes.append('{}="{}"'.format(lft, rgt))
if inside is not None: if inside is not None:
return "<{kind} {attributes}>{inside}</{kind}>".format( return format_html("<{kind} {attributes}>{inside}</{kind}>",
kind=kind, attributes=" ".join(attributes), inside=inside) kind=kind, attributes=attributes, inside=inside)
return "<{} {}/>".format(kind, " ".join(attributes)) return format_html("<{} {}/>", kind, attributes)
class LogAdmin(CommonAdmin): class LogAdmin(CommonAdmin):

View File

@@ -2,7 +2,7 @@ import textwrap
from django.conf import settings from django.conf import settings
from django.core.checks import Error, register from django.core.checks import Error, register
from django.db.utils import OperationalError from django.db.utils import OperationalError, ProgrammingError
@register() @register()
@@ -14,7 +14,7 @@ def changed_password_check(app_configs, **kwargs):
try: try:
encrypted_doc = Document.objects.filter( encrypted_doc = Document.objects.filter(
storage_type=Document.STORAGE_TYPE_GPG).first() storage_type=Document.STORAGE_TYPE_GPG).first()
except OperationalError: except (OperationalError, ProgrammingError):
return [] # No documents table yet return [] # No documents table yet
if encrypted_doc: if encrypted_doc:

View File

@@ -1,11 +1,11 @@
from django_filters.rest_framework import CharFilter, FilterSet from django_filters.rest_framework import CharFilter, FilterSet, BooleanFilter
from .models import Correspondent, Document, Tag from .models import Correspondent, Document, Tag
class CorrespondentFilterSet(FilterSet): class CorrespondentFilterSet(FilterSet):
class Meta(object): class Meta:
model = Correspondent model = Correspondent
fields = { fields = {
"name": [ "name": [
@@ -18,7 +18,7 @@ class CorrespondentFilterSet(FilterSet):
class TagFilterSet(FilterSet): class TagFilterSet(FilterSet):
class Meta(object): class Meta:
model = Tag model = Tag
fields = { fields = {
"name": [ "name": [
@@ -42,12 +42,18 @@ class DocumentFilterSet(FilterSet):
) )
} }
correspondent__name = CharFilter(name="correspondent__name", **CHAR_KWARGS) correspondent__name = CharFilter(
correspondent__slug = CharFilter(name="correspondent__slug", **CHAR_KWARGS) field_name="correspondent__name", **CHAR_KWARGS)
tags__name = CharFilter(name="tags__name", **CHAR_KWARGS) correspondent__slug = CharFilter(
tags__slug = CharFilter(name="tags__slug", **CHAR_KWARGS) field_name="correspondent__slug", **CHAR_KWARGS)
tags__name = CharFilter(
field_name="tags__name", **CHAR_KWARGS)
tags__slug = CharFilter(
field_name="tags__slug", **CHAR_KWARGS)
tags__empty = BooleanFilter(
field_name="tags", lookup_expr="isnull", distinct=True)
class Meta(object): class Meta:
model = Document model = Document
fields = { fields = {
"title": [ "title": [

View File

@@ -1,7 +1,5 @@
import datetime
import logging import logging
import os import os
import sys
import time import time
from django.conf import settings from django.conf import settings
@@ -13,7 +11,7 @@ from ...mail import MailFetcher, MailFetcherError
try: try:
from inotify_simple import INotify, flags from inotify_simple import INotify, flags
except ImportError: except ImportError:
pass INotify = flags = None
class Command(BaseCommand): class Command(BaseCommand):
@@ -62,7 +60,8 @@ class Command(BaseCommand):
parser.add_argument( parser.add_argument(
"--no-inotify", "--no-inotify",
action="store_true", action="store_true",
help="Don't use inotify, even if it's available." help="Don't use inotify, even if it's available.",
default=False
) )
def handle(self, *args, **options): def handle(self, *args, **options):
@@ -71,8 +70,7 @@ class Command(BaseCommand):
directory = options["directory"] directory = options["directory"]
loop_time = options["loop_time"] loop_time = options["loop_time"]
mail_delta = options["mail_delta"] * 60 mail_delta = options["mail_delta"] * 60
use_inotify = (not options["no_inotify"] use_inotify = INotify is not None and options["no_inotify"] is False
and "inotify_simple" in sys.modules)
try: try:
self.file_consumer = Consumer(consume=directory) self.file_consumer = Consumer(consume=directory)

View File

@@ -32,7 +32,6 @@ def realign_senders(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('documents', '0002_auto_20151226_1316'), ('documents', '0002_auto_20151226_1316'),
] ]

View File

@@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
atomic = False
dependencies = [ dependencies = [
('documents', '0010_log'), ('documents', '0010_log'),
] ]

View File

@@ -112,7 +112,6 @@ def move_documents_and_create_thumbnails(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('documents', '0011_auto_20160303_1929'), ('documents', '0011_auto_20160303_1929'),
] ]

View File

@@ -128,7 +128,6 @@ def do_nothing(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('documents', '0013_auto_20160325_2111'), ('documents', '0013_auto_20160325_2111'),
] ]

View File

@@ -15,7 +15,6 @@ def reverse_func(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('documents', '0018_auto_20170715_1712'), ('documents', '0018_auto_20170715_1712'),
] ]

View File

@@ -11,8 +11,8 @@ def set_added_time_to_created_time(apps, schema_editor):
doc.added = doc.created doc.added = doc.created
doc.save() doc.save()
class Migration(migrations.Migration):
class Migration(migrations.Migration):
dependencies = [ dependencies = [
('documents', '0019_add_consumer_user'), ('documents', '0019_add_consumer_user'),
] ]

View File

@@ -1,22 +1,25 @@
# coding=utf-8 # coding=utf-8
import dateutil.parser
import logging import logging
import os import os
import re import re
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
from fuzzywuzzy import fuzz
import dateutil.parser
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.utils import timezone from django.utils import timezone
from fuzzywuzzy import fuzz
from .managers import LogManager from .managers import LogManager
try:
from django.core.urlresolvers import reverse
except ImportError:
from django.urls import reverse
class MatchingModel(models.Model): class MatchingModel(models.Model):
@@ -59,6 +62,7 @@ class MatchingModel(models.Model):
class Meta: class Meta:
abstract = True abstract = True
ordering = ("name",)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -131,7 +135,7 @@ class MatchingModel(models.Model):
Example: Example:
' some random words "with quotes " and spaces' ' some random words "with quotes " and spaces'
==> ==>
["some", "random", "words", "with\s+quotes", "and", "spaces"] ["some", "random", "words", "with+quotes", "and", "spaces"]
""" """
findterms = re.compile(r'"([^"]+)"|(\S+)').findall findterms = re.compile(r'"([^"]+)"|(\S+)').findall
normspace = re.compile(r"\s+").sub normspace = re.compile(r"\s+").sub
@@ -188,7 +192,11 @@ class Document(models.Model):
TYPE_JPG = "jpg" TYPE_JPG = "jpg"
TYPE_GIF = "gif" TYPE_GIF = "gif"
TYPE_TIF = "tiff" TYPE_TIF = "tiff"
TYPES = (TYPE_PDF, TYPE_PNG, TYPE_JPG, TYPE_GIF, TYPE_TIF,) TYPE_TXT = "txt"
TYPE_CSV = "csv"
TYPE_MD = "md"
TYPES = (TYPE_PDF, TYPE_PNG, TYPE_JPG, TYPE_GIF, TYPE_TIF,
TYPE_TXT, TYPE_CSV, TYPE_MD)
STORAGE_TYPE_UNENCRYPTED = "unencrypted" STORAGE_TYPE_UNENCRYPTED = "unencrypted"
STORAGE_TYPE_GPG = "gpg" STORAGE_TYPE_GPG = "gpg"
@@ -361,51 +369,52 @@ class FileInfo:
) )
) )
formats = "pdf|jpe?g|png|gif|tiff?|te?xt|md|csv"
REGEXES = OrderedDict([ REGEXES = OrderedDict([
("created-correspondent-title-tags", re.compile( ("created-correspondent-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<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>{})$".format(formats),
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>{})$".format(formats),
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>{})$".format(formats),
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>{})$".format(formats),
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>{})$".format(formats),
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>{})$".format(formats),
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>{})$".format(formats),
flags=re.IGNORECASE flags=re.IGNORECASE
)) ))
]) ])
@@ -442,8 +451,10 @@ class FileInfo:
def _get_tags(cls, tags): def _get_tags(cls, tags):
r = [] r = []
for t in tags.split(","): for t in tags.split(","):
r.append( r.append(Tag.objects.get_or_create(
Tag.objects.get_or_create(slug=t, defaults={"name": t})[0]) slug=t.lower(),
defaults={"name": t}
)[0])
return tuple(r) return tuple(r)
@classmethod @classmethod

View File

@@ -1,9 +1,25 @@
import logging import logging
import shutil import shutil
import tempfile import tempfile
import re
from django.conf import settings from django.conf import settings
# This regular expression will try to find dates in the document at
# hand and will match the following formats:
# - XX.YY.ZZZZ with XX + YY being 1 or 2 and ZZZZ being 2 or 4 digits
# - XX/YY/ZZZZ with XX + YY being 1 or 2 and ZZZZ being 2 or 4 digits
# - XX-YY-ZZZZ with XX + YY being 1 or 2 and ZZZZ being 2 or 4 digits
# - XX. MONTH ZZZZ with XX being 1 or 2 and ZZZZ being 2 or 4 digits
# - MONTH ZZZZ, with ZZZZ being 4 digits
# - MONTH XX, ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits
DATE_REGEX = re.compile(
r'\b([0-9]{1,2})[\.\/-]([0-9]{1,2})[\.\/-]([0-9]{4}|[0-9]{2})\b|' +
r'\b([0-9]{1,2}[\. ]+[^ ]{3,9} ([0-9]{4}|[0-9]{2}))\b|' +
r'\b([^\W\d_]{3,9} [0-9]{1,2}, ([0-9]{4}))\b|' +
r'\b([^\W\d_]{3,9} [0-9]{4})\b'
)
class ParseError(Exception): class ParseError(Exception):
pass pass

View File

@@ -5,14 +5,14 @@ from .models import Correspondent, Tag, Document, Log
class CorrespondentSerializer(serializers.HyperlinkedModelSerializer): class CorrespondentSerializer(serializers.HyperlinkedModelSerializer):
class Meta(object): class Meta:
model = Correspondent model = Correspondent
fields = ("id", "slug", "name") fields = ("id", "slug", "name")
class TagSerializer(serializers.HyperlinkedModelSerializer): class TagSerializer(serializers.HyperlinkedModelSerializer):
class Meta(object): class Meta:
model = Tag model = Tag
fields = ( fields = (
"id", "slug", "name", "colour", "match", "matching_algorithm") "id", "slug", "name", "colour", "match", "matching_algorithm")
@@ -34,7 +34,7 @@ class DocumentSerializer(serializers.ModelSerializer):
view_name="drf:correspondent-detail", allow_null=True) view_name="drf:correspondent-detail", allow_null=True)
tags = TagsField(view_name="drf:tag-detail", many=True) tags = TagsField(view_name="drf:tag-detail", many=True)
class Meta(object): class Meta:
model = Document model = Document
fields = ( fields = (
"id", "id",
@@ -57,7 +57,7 @@ class LogSerializer(serializers.ModelSerializer):
time = serializers.DateTimeField() time = serializers.DateTimeField()
messages = serializers.CharField() messages = serializers.CharField()
class Meta(object): class Meta:
model = Log model = Log
fields = ( fields = (
"time", "time",

View File

@@ -3,7 +3,7 @@ from unittest import mock
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..consumer import Consumer from ..consumer import Consumer
from ..models import FileInfo from ..models import FileInfo, Tag
class TestConsumer(TestCase): class TestConsumer(TestCase):
@@ -190,6 +190,20 @@ class TestAttributes(TestCase):
() ()
) )
def test_case_insensitive_tag_creation(self):
"""
Tags should be detected and created as lower case.
:return:
"""
path = "Title - Correspondent - tAg1,TAG2.pdf"
self.assertEqual(len(FileInfo.from_path(path).tags), 2)
path = "Title - Correspondent - tag1,tag2.pdf"
self.assertEqual(len(FileInfo.from_path(path).tags), 2)
self.assertEqual(Tag.objects.all().count(), 2)
class TestFieldPermutations(TestCase): class TestFieldPermutations(TestCase):

View File

@@ -166,7 +166,7 @@ class TestMatching(TestCase):
def test_match_regex(self): def test_match_regex(self):
self._test_matching( self._test_matching(
"alpha\w+gamma", r"alpha\w+gamma",
"MATCH_REGEX", "MATCH_REGEX",
( (
"I have alpha_and_gamma in me", "I have alpha_and_gamma in me",

View File

@@ -1,6 +1,8 @@
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from django.views.generic import DetailView, FormView, TemplateView from django.views.generic import DetailView, FormView, TemplateView
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.conf import settings
from paperless.db import GnuPG from paperless.db import GnuPG
from paperless.mixins import SessionOrBasicAuthMixin from paperless.mixins import SessionOrBasicAuthMixin
from paperless.views import StandardPagination from paperless.views import StandardPagination
@@ -48,6 +50,9 @@ class FetchView(SessionOrBasicAuthMixin, DetailView):
Document.TYPE_JPG: "image/jpeg", Document.TYPE_JPG: "image/jpeg",
Document.TYPE_GIF: "image/gif", Document.TYPE_GIF: "image/gif",
Document.TYPE_TIF: "image/tiff", Document.TYPE_TIF: "image/tiff",
Document.TYPE_CSV: "text/csv",
Document.TYPE_MD: "text/markdown",
Document.TYPE_TXT: "text/plain"
} }
if self.kwargs["kind"] == "thumb": if self.kwargs["kind"] == "thumb":
@@ -60,8 +65,11 @@ class FetchView(SessionOrBasicAuthMixin, DetailView):
self._get_raw_data(self.object.source_file), self._get_raw_data(self.object.source_file),
content_type=content_types[self.object.file_type] content_type=content_types[self.object.file_type]
) )
response["Content-Disposition"] = 'attachment; filename="{}"'.format(
self.object.file_name) DISPOSITION = 'inline' if settings.INLINE_DOC else 'attachment'
response["Content-Disposition"] = '{}; filename="{}"'.format(
DISPOSITION, self.object.file_name)
return response return response

View File

@@ -22,6 +22,14 @@ elif os.path.exists("/usr/local/etc/paperless.conf"):
load_dotenv("/usr/local/etc/paperless.conf") load_dotenv("/usr/local/etc/paperless.conf")
def __get_boolean(key):
"""
Return a boolean value based on whatever the user has supplied in the
environment based on whether the value "looks like" it's True or not.
"""
return bool(os.getenv(key, "NO").lower() in ("yes", "y", "1", "t", "true"))
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -61,13 +69,14 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"corsheaders",
"django_extensions", "django_extensions",
"documents.apps.DocumentsConfig", "documents.apps.DocumentsConfig",
"reminders.apps.RemindersConfig", "reminders.apps.RemindersConfig",
"paperless_tesseract.apps.PaperlessTesseractConfig", "paperless_tesseract.apps.PaperlessTesseractConfig",
"paperless_text.apps.PaperlessTextConfig",
"flat_responsive", # TODO: Remove as of Django 2.x
"django.contrib.admin", "django.contrib.admin",
"rest_framework", "rest_framework",
@@ -79,24 +88,24 @@ INSTALLED_APPS = [
if os.getenv("PAPERLESS_INSTALLED_APPS"): if os.getenv("PAPERLESS_INSTALLED_APPS"):
INSTALLED_APPS += os.getenv("PAPERLESS_INSTALLED_APPS").split(",") INSTALLED_APPS += os.getenv("PAPERLESS_INSTALLED_APPS").split(",")
MIDDLEWARE = [
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
# We allow CORS from localhost:8080
CORS_ORIGIN_WHITELIST = tuple(os.getenv("PAPERLESS_CORS_ALLOWED_HOSTS", "localhost:8080").split(","))
# If auth is disabled, we just use our "bypass" authentication middleware # If auth is disabled, we just use our "bypass" authentication middleware
if bool(os.getenv("PAPERLESS_DISABLE_LOGIN", "false").lower() in ("yes", "y", "1", "t", "true")): if bool(os.getenv("PAPERLESS_DISABLE_LOGIN", "false").lower() in ("yes", "y", "1", "t", "true")):
_index = MIDDLEWARE_CLASSES.index("django.contrib.auth.middleware.AuthenticationMiddleware") _index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
MIDDLEWARE_CLASSES[_index] = "paperless.middleware.Middleware" MIDDLEWARE[_index] = "paperless.middleware.Middleware"
MIDDLEWARE_CLASSES.remove("django.contrib.auth.middleware.SessionAuthenticationMiddleware")
ROOT_URLCONF = 'paperless.urls' ROOT_URLCONF = 'paperless.urls'
@@ -221,12 +230,12 @@ OCR_LANGUAGE = os.getenv("PAPERLESS_OCR_LANGUAGE", "eng")
OCR_THREADS = os.getenv("PAPERLESS_OCR_THREADS") OCR_THREADS = os.getenv("PAPERLESS_OCR_THREADS")
# OCR all documents? # OCR all documents?
OCR_ALWAYS = bool(os.getenv("PAPERLESS_OCR_ALWAYS", "NO").lower() in ("yes", "y", "1", "t", "true")) # NOQA OCR_ALWAYS = __get_boolean("PAPERLESS_OCR_ALWAYS")
# If this is true, any failed attempts to OCR a PDF will result in the PDF # If this is true, any failed attempts to OCR a PDF will result in the PDF
# being indexed anyway, with whatever we could get. If it's False, the file # being indexed anyway, with whatever we could get. If it's False, the file
# will simply be left in the CONSUMPTION_DIR. # will simply be left in the CONSUMPTION_DIR.
FORGIVING_OCR = bool(os.getenv("PAPERLESS_FORGIVING_OCR", "YES").lower() in ("yes", "y", "1", "t", "true")) # NOQA FORGIVING_OCR = __get_boolean("PAPERLESS_FORGIVING_OCR")
# GNUPG needs a home directory for some reason # GNUPG needs a home directory for some reason
GNUPG_HOME = os.getenv("HOME", "/tmp") GNUPG_HOME = os.getenv("HOME", "/tmp")
@@ -270,6 +279,9 @@ PASSPHRASE = os.getenv("PAPERLESS_PASSPHRASE")
PRE_CONSUME_SCRIPT = os.getenv("PAPERLESS_PRE_CONSUME_SCRIPT") PRE_CONSUME_SCRIPT = os.getenv("PAPERLESS_PRE_CONSUME_SCRIPT")
POST_CONSUME_SCRIPT = os.getenv("PAPERLESS_POST_CONSUME_SCRIPT") POST_CONSUME_SCRIPT = os.getenv("PAPERLESS_POST_CONSUME_SCRIPT")
# Whether to display a selected document inline, or download it as attachment:
INLINE_DOC = __get_boolean("PAPERLESS_INLINE_DOC")
# The number of items on each page in the web UI. This value must be a # The number of items on each page in the web UI. This value must be a
# positive integer, but if you don't define one in paperless.conf, a default of # positive integer, but if you don't define one in paperless.conf, a default of
# 100 will be used. # 100 will be used.

View File

@@ -28,9 +28,11 @@ urlpatterns = [
# API # API
url( url(
r"^api/auth/", r"^api/auth/",
include('rest_framework.urls', namespace="rest_framework") include(
('rest_framework.urls', 'rest_framework'),
namespace="rest_framework")
), ),
url(r"^api/", include(router.urls, namespace="drf")), url(r"^api/", include((router.urls, 'drf'), namespace="drf")),
# File downloads # File downloads
url( url(

View File

@@ -1 +1 @@
__version__ = (2, 1, 0) __version__ = (2, 3, 0)

View File

@@ -14,7 +14,7 @@ from pyocr.libtesseract.tesseract_raw import \
from pyocr.tesseract import TesseractError from pyocr.tesseract import TesseractError
import pdftotext import pdftotext
from documents.parsers import DocumentParser, ParseError from documents.parsers import DocumentParser, ParseError, DATE_REGEX
from .languages import ISO639 from .languages import ISO639
@@ -50,10 +50,11 @@ class RasterisedDocumentParser(DocumentParser):
self.CONVERT, self.CONVERT,
"-scale", "500x5000", "-scale", "500x5000",
"-alpha", "remove", "-alpha", "remove",
self.document_path, os.path.join(self.tempdir, "convert-%04d.png") "{}[0]".format(self.document_path),
os.path.join(self.tempdir, "convert.png")
) )
return os.path.join(self.tempdir, "convert-0000.png") return os.path.join(self.tempdir, "convert.png")
def _is_ocred(self): def _is_ocred(self):
@@ -210,22 +211,8 @@ class RasterisedDocumentParser(DocumentParser):
except ParseError as e: except ParseError as e:
return None return None
# This regular expression will try to find dates in the document at
# hand and will match the following formats:
# - XX.YY.ZZZZ with XX + YY being 1 or 2 and ZZZZ being 2 or 4 digits
# - XX/YY/ZZZZ with XX + YY being 1 or 2 and ZZZZ being 2 or 4 digits
# - XX-YY-ZZZZ with XX + YY being 1 or 2 and ZZZZ being 2 or 4 digits
# - XX. MONTH ZZZZ with XX being 1 or 2 and ZZZZ being 2 or 4 digits
# - MONTH ZZZZ, with ZZZZ being 4 digits
# - MONTH XX, ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits
pattern = re.compile(
r'\b([0-9]{1,2})[\.\/-]([0-9]{1,2})[\.\/-]([0-9]{4}|[0-9]{2})\b|' +
r'\b([0-9]{1,2}[\. ]+[^ ]{3,9} ([0-9]{4}|[0-9]{2}))\b|' +
r'\b([^\W\d_]{3,9} [0-9]{1,2}, ([0-9]{4}))\b|' +
r'\b([^\W\d_]{3,9} [0-9]{4})\b')
# Iterate through all regex matches and try to parse the date # Iterate through all regex matches and try to parse the date
for m in re.finditer(pattern, text): for m in re.finditer(DATE_REGEX, text):
datestring = m.group(0) datestring = m.group(0)
try: try:
@@ -272,8 +259,9 @@ def run_unpaper(args):
def strip_excess_whitespace(text): def strip_excess_whitespace(text):
collapsed_spaces = re.sub(r"([^\S\r\n]+)", " ", text) collapsed_spaces = re.sub(r"([^\S\r\n]+)", " ", text)
no_leading_whitespace = re.sub( no_leading_whitespace = re.sub(
"([\n\r]+)([^\S\n\r]+)", '\\1', collapsed_spaces) r"([\n\r]+)([^\S\n\r]+)", '\\1', collapsed_spaces)
no_trailing_whitespace = re.sub("([^\S\n\r]+)$", '', no_leading_whitespace) no_trailing_whitespace = re.sub(
r"([^\S\n\r]+)$", '', no_leading_whitespace)
return no_trailing_whitespace return no_trailing_whitespace

View File

@@ -5,7 +5,7 @@ from .parsers import RasterisedDocumentParser
class ConsumerDeclaration: class ConsumerDeclaration:
MATCHING_FILES = re.compile("^.*\.(pdf|jpe?g|gif|png|tiff?|pnm|bmp)$") MATCHING_FILES = re.compile(r"^.*\.(pdf|jpe?g|gif|png|tiff?|pnm|bmp)$")
@classmethod @classmethod
def handle(cls, sender, **kwargs): def handle(cls, sender, **kwargs):

View File

@@ -33,7 +33,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_2(self): def test_date_format_2(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -43,7 +43,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_3(self): def test_date_format_3(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -53,7 +53,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_4(self): def test_date_format_4(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -66,7 +66,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_5(self): def test_date_format_5(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -80,7 +80,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_6(self): def test_date_format_6(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -100,7 +100,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_7(self): def test_date_format_7(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -117,7 +117,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_8(self): def test_date_format_8(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -138,7 +138,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_date_format_9(self): def test_date_format_9(self):
input_file = os.path.join(self.SAMPLE_FILES, "") input_file = os.path.join(self.SAMPLE_FILES, "")
@@ -153,7 +153,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_get_text_1_pdf(self): def test_get_text_1_pdf(self):
input_file = os.path.join(self.SAMPLE_FILES, "tests_date_1.pdf") input_file = os.path.join(self.SAMPLE_FILES, "tests_date_1.pdf")
@@ -359,7 +359,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_get_text_8_pdf(self): def test_get_text_8_pdf(self):
input_file = os.path.join(self.SAMPLE_FILES, "tests_date_8.pdf") input_file = os.path.join(self.SAMPLE_FILES, "tests_date_8.pdf")
@@ -373,7 +373,7 @@ class TestDate(TestCase):
@mock.patch( @mock.patch(
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH", "paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
SAMPLE_FILES SCRATCH
) )
def test_get_text_9_pdf(self): def test_get_text_9_pdf(self):
input_file = os.path.join(self.SAMPLE_FILES, "tests_date_9.pdf") input_file = os.path.join(self.SAMPLE_FILES, "tests_date_9.pdf")

View File

View File

@@ -0,0 +1,16 @@
from django.apps import AppConfig
class PaperlessTextConfig(AppConfig):
name = "paperless_text"
def ready(self):
from documents.signals import document_consumer_declaration
from .signals import ConsumerDeclaration
document_consumer_declaration.connect(ConsumerDeclaration.handle)
AppConfig.ready(self)

View File

@@ -0,0 +1,131 @@
import os
import re
import subprocess
import dateparser
from django.conf import settings
from documents.parsers import DocumentParser, ParseError, DATE_REGEX
class TextDocumentParser(DocumentParser):
"""
This parser directly parses a text document (.txt, .md, or .csv)
"""
CONVERT = settings.CONVERT_BINARY
THREADS = int(settings.OCR_THREADS) if settings.OCR_THREADS else None
UNPAPER = settings.UNPAPER_BINARY
DATE_ORDER = settings.DATE_ORDER
DEFAULT_OCR_LANGUAGE = settings.OCR_LANGUAGE
OCR_ALWAYS = settings.OCR_ALWAYS
def __init__(self, path):
super().__init__(path)
self._text = None
def get_thumbnail(self):
"""
The thumbnail of a txt is just a 500px wide image of the text
rendered onto a letter-sized page.
"""
# The below is heavily cribbed from https://askubuntu.com/a/590951
bg_color = "white" # bg color
text_color = "black" # text color
psize = [500, 647] # icon size
n_lines = 50 # number of lines to show
output_file = os.path.join(self.tempdir, "convert-txt.png")
temp_bg = os.path.join(self.tempdir, "bg.png")
temp_txlayer = os.path.join(self.tempdir, "tx.png")
picsize = "x".join([str(n) for n in psize])
txsize = "x".join([str(n - 8) for n in psize])
def create_bg():
work_size = ",".join([str(n - 1) for n in psize])
r = str(round(psize[0] / 10))
rounded = ",".join([r, r])
run_command(self.CONVERT, "-size ", picsize, ' xc:none -draw ',
'"fill ', bg_color, ' roundrectangle 0,0,',
work_size, ",", rounded, '" ', temp_bg)
def read_text():
with open(self.document_path, 'r') as src:
lines = [l.strip() for l in src.readlines()]
text = "\n".join([l for l in lines[:n_lines]])
return text.replace('"', "'")
def create_txlayer():
run_command(self.CONVERT,
"-background none",
"-fill",
text_color,
"-pointsize", "12",
"-border 4 -bordercolor none",
"-size ", txsize,
' caption:"', read_text(), '" ',
temp_txlayer)
create_txlayer()
create_bg()
run_command(self.CONVERT, temp_bg, temp_txlayer,
"-background None -layers merge ", output_file)
return output_file
def get_text(self):
if self._text is not None:
return self._text
with open(self.document_path, 'r') as f:
self._text = f.read()
return self._text
def get_date(self):
date = None
datestring = None
try:
text = self.get_text()
except ParseError as e:
return None
# Iterate through all regex matches and try to parse the date
for m in re.finditer(DATE_REGEX, text):
datestring = m.group(0)
try:
date = dateparser.parse(
datestring,
settings={'DATE_ORDER': self.DATE_ORDER,
'PREFER_DAY_OF_MONTH': 'first',
'RETURN_AS_TIMEZONE_AWARE': True})
except TypeError:
# Skip all matches that do not parse to a proper date
continue
if date is not None:
break
if date is not None:
self.log("info", "Detected document date " + date.isoformat() +
" based on string " + datestring)
else:
self.log("info", "Unable to detect date for document")
return date
def run_command(*args):
environment = os.environ.copy()
if settings.CONVERT_MEMORY_LIMIT:
environment["MAGICK_MEMORY_LIMIT"] = settings.CONVERT_MEMORY_LIMIT
if settings.CONVERT_TMPDIR:
environment["MAGICK_TMPDIR"] = settings.CONVERT_TMPDIR
if not subprocess.Popen(' '.join(args), env=environment,
shell=True).wait() == 0:
raise ParseError("Convert failed at {}".format(args))

View File

@@ -0,0 +1,23 @@
import re
from .parsers import TextDocumentParser
class ConsumerDeclaration:
MATCHING_FILES = re.compile(r"^.*\.(te?xt|md|csv)$")
@classmethod
def handle(cls, sender, **kwargs):
return cls.test
@classmethod
def test(cls, doc):
if cls.MATCHING_FILES.match(doc.lower()):
return {
"parser": TextDocumentParser,
"weight": 10
}
return None

View File

@@ -3,6 +3,8 @@ from django.db import models
class Reminder(models.Model): class Reminder(models.Model):
document = models.ForeignKey("documents.Document") document = models.ForeignKey(
"documents.Document", on_delete=models.PROTECT
)
date = models.DateTimeField() date = models.DateTimeField()
note = models.TextField(blank=True) note = models.TextField(blank=True)