mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -05:00
Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ee20af71e8 | ||
![]() |
3c8aa3ba42 | ||
![]() |
778ffa488d | ||
![]() |
0868390d63 | ||
![]() |
d5180fe5e1 | ||
![]() |
08174a6b52 | ||
![]() |
f5e725c691 | ||
![]() |
2400245b96 | ||
![]() |
729f005600 | ||
![]() |
39afe41f08 | ||
![]() |
2d4008371b | ||
![]() |
218809ce15 | ||
![]() |
7db4410c1b | ||
![]() |
f1e1bb4deb | ||
![]() |
cccc9e1a24 | ||
![]() |
39ef81d398 | ||
![]() |
ec862ed526 | ||
![]() |
efb0157337 | ||
![]() |
efc57852d1 | ||
![]() |
0b9c4f9963 | ||
![]() |
633d2b376f | ||
![]() |
91cecd47af | ||
![]() |
b05fd4870e | ||
![]() |
40e79a731f | ||
![]() |
6cd06f6c8a | ||
![]() |
b6a870c0e5 | ||
![]() |
160c256327 | ||
![]() |
fcd36c8415 | ||
![]() |
70608f7e31 | ||
![]() |
4e5ee24618 | ||
![]() |
1bb80548d2 | ||
![]() |
96268655d2 | ||
![]() |
be2cbebaf7 | ||
![]() |
c79583dedb | ||
![]() |
24d3e7f9d3 | ||
![]() |
088a631f6a | ||
![]() |
a9bb78c4ae | ||
![]() |
3f4ac1f2f1 | ||
![]() |
6c3afd21b9 | ||
![]() |
e7daf7dae4 | ||
![]() |
ba1f437e0b | ||
![]() |
826db170d3 | ||
![]() |
04816f556f | ||
![]() |
f99db14a21 | ||
![]() |
c75a1e9eca | ||
![]() |
2894d105cb | ||
![]() |
f31d535da8 | ||
![]() |
619878b2f6 | ||
![]() |
5db49a3710 | ||
![]() |
f3654310bd | ||
![]() |
10054f978a | ||
![]() |
5308f2166d | ||
![]() |
03ec9f5a06 | ||
![]() |
454bf7595e | ||
![]() |
b916fe13e1 | ||
![]() |
484cbc1bf2 | ||
![]() |
1d61c9cd79 | ||
![]() |
3b72d38440 | ||
![]() |
631d316985 | ||
![]() |
742b01d1f5 | ||
![]() |
d37aabfb06 | ||
![]() |
b3624f6375 | ||
![]() |
d6d8537b69 | ||
![]() |
90cd9f3eb7 | ||
![]() |
a0240cace3 | ||
![]() |
988adf963a | ||
![]() |
3d188ec623 | ||
![]() |
c9f35a7da2 | ||
![]() |
a1cb67c4ce | ||
![]() |
c37f642cff | ||
![]() |
9df06fbb12 | ||
![]() |
0abf637c67 | ||
![]() |
27a936f9bf | ||
![]() |
6e1f2b3f03 | ||
![]() |
5643d89270 | ||
![]() |
52b0249d71 | ||
![]() |
2ab2c37f5a | ||
![]() |
f72fa43e86 | ||
![]() |
c0ad6cd58a | ||
![]() |
b79caa64d0 | ||
![]() |
e5b7e93eff | ||
![]() |
d8740ee5ca | ||
![]() |
cdc07cf153 | ||
![]() |
da6dc2ad5b | ||
![]() |
885dbf67d5 | ||
![]() |
02b40a54e0 | ||
![]() |
3b6a3219f5 | ||
![]() |
8783c2af88 | ||
![]() |
6cedbb3307 | ||
![]() |
6fd9995aa1 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -59,8 +59,10 @@ target/
|
||||
|
||||
# Stored PDFs
|
||||
media/documents/*.gpg
|
||||
media/documents/thumbnails/*.gpg
|
||||
media/documents/originals/*.gpg
|
||||
media/documents/thumbnails/*
|
||||
media/documents/originals/*
|
||||
media/overrides.css
|
||||
media/overrides.js
|
||||
|
||||
# Sqlite database
|
||||
db.sqlite3
|
||||
|
@@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
|
||||
|
||||
## 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
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
|
10
Dockerfile
10
Dockerfile
@@ -4,11 +4,8 @@ LABEL maintainer="The Paperless Project https://github.com/danielquinn/paperless
|
||||
contributors="Guy Addadi <addadi@gmail.com>, Pit Kleyersburg <pitkley@googlemail.com>, \
|
||||
Sven Fischer <git-dev@linux4tw.de>"
|
||||
|
||||
# Copy application
|
||||
# Copy requirements file and init script
|
||||
COPY requirements.txt /usr/src/paperless/
|
||||
COPY src/ /usr/src/paperless/src/
|
||||
COPY data/ /usr/src/paperless/data/
|
||||
COPY media/ /usr/src/paperless/media/
|
||||
COPY scripts/docker-entrypoint.sh /sbin/docker-entrypoint.sh
|
||||
|
||||
# Set export and consumption directories
|
||||
@@ -44,3 +41,8 @@ VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/consume", "/exp
|
||||
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||
CMD ["--help"]
|
||||
|
||||
# Copy application
|
||||
COPY src/ /usr/src/paperless/src/
|
||||
COPY data/ /usr/src/paperless/data/
|
||||
COPY media/ /usr/src/paperless/media/
|
||||
|
||||
|
7
Pipfile
7
Pipfile
@@ -4,20 +4,20 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
django = "<2.0,>=1.11"
|
||||
django = "<2.1,>=2.0"
|
||||
pillow = "*"
|
||||
coveralls = "*"
|
||||
dateparser = "*"
|
||||
django-cors-headers = "*"
|
||||
django-crispy-forms = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "*"
|
||||
django-flat-responsive = "*"
|
||||
djangorestframework = "*"
|
||||
factory-boy = "*"
|
||||
"flake8" = "*"
|
||||
filemagic = "*"
|
||||
fuzzywuzzy = {extras = ["speedup"], version = "==0.15.0"}
|
||||
gunicorn = "*"
|
||||
inotify-simple = "*"
|
||||
langdetect = "*"
|
||||
pdftotext = "*"
|
||||
pyocr = "*"
|
||||
@@ -35,3 +35,4 @@ pytest-xdist = "*"
|
||||
|
||||
[dev-packages]
|
||||
ipython = "*"
|
||||
sphinx = "*"
|
||||
|
492
Pipfile.lock
generated
492
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "928fbb4c8952128aef7a2ed2707ce510d31d49df96cfc5f08959698edff6e67f"
|
||||
"sha256": "e20c2294bcafd346ee57901df94a515a12976ed192dc37df848b39b56bdd1f4b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -16,24 +16,33 @@
|
||||
"default": {
|
||||
"apipkg": {
|
||||
"hashes": [
|
||||
"sha256:2e38399dbe842891fe85392601aab8f40a8f4cc5a9053c326de35a1cc0297ac6",
|
||||
"sha256:65d2aa68b28e7d31233bb2ba8eb31cda40e4671f8ac2d6b241e358c9652a74b9"
|
||||
"sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6",
|
||||
"sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"
|
||||
],
|
||||
"version": "==1.4"
|
||||
"markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==1.5"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
|
||||
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9",
|
||||
"sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"
|
||||
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
|
||||
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
|
||||
],
|
||||
"version": "==17.4.0"
|
||||
"version": "==18.2.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296",
|
||||
"sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d"
|
||||
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
|
||||
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
|
||||
],
|
||||
"version": "==2018.1.18"
|
||||
"version": "==2018.8.24"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -46,11 +55,11 @@
|
||||
"hashes": [
|
||||
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
|
||||
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
|
||||
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a",
|
||||
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
|
||||
"sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
|
||||
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
|
||||
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2",
|
||||
"sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
|
||||
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
|
||||
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
|
||||
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
|
||||
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
|
||||
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
|
||||
@@ -70,26 +79,22 @@
|
||||
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
|
||||
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
|
||||
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
|
||||
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
|
||||
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
|
||||
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
|
||||
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
|
||||
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
|
||||
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
|
||||
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
|
||||
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77",
|
||||
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
|
||||
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
|
||||
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.2.*' and python_version < '4' and python_version != '3.1.*'",
|
||||
"version": "==4.5.1"
|
||||
},
|
||||
"coveralls": {
|
||||
"hashes": [
|
||||
"sha256:32569a43c9dbc13fa8199247580a4ab182ef439f51f65bb7f8316d377a1340e8",
|
||||
"sha256:664794748d2e5673e347ec476159a9d87f43e0d2d44950e98ed0e27b98da8346"
|
||||
"sha256:9dee67e78ec17b36c52b778247762851c8e19a893c9a14e921a2fc37f05fac22",
|
||||
"sha256:aec5a1f5e34224b9089664a1b62217732381c7de361b6ed1b3c394d7187b352a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"dateparser": {
|
||||
"hashes": [
|
||||
@@ -101,11 +106,19 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:056fe5b9e1f8f7fed9bb392919d64f6b33b3a71cfb0f170a90ee277a6ed32bc2",
|
||||
"sha256:4d398c7b02761e234bbde490aea13ea94cb539ceeb72805b72303f348682f2eb"
|
||||
"sha256:0c5b65847d00845ee404bbc0b4a85686f15eb3001ffddda3db4e9baa265bf136",
|
||||
"sha256:68aeea369a8130259354b6ba1fa9babe0c5ee6bced505dea4afcd00f765ae38b"
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
@@ -117,26 +130,19 @@
|
||||
},
|
||||
"django-extensions": {
|
||||
"hashes": [
|
||||
"sha256:37a543af370ee3b0721ff50442d33c357dd083e6ea06c5b94a199283b6f9e361",
|
||||
"sha256:bc9f2946c117bb2f49e5e0633eba783787790ae810ea112fe7fd82fa64de2ff1"
|
||||
"sha256:1f626353a11479014bfe0d77e76d8f866ebca1bb5d595cb57b776230b9e0eb92",
|
||||
"sha256:f21b898598a1628cb73017fb9672e2c5e624133be9764f0eb138e0abf8a62b62"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.6"
|
||||
"version": "==2.1.2"
|
||||
},
|
||||
"django-filter": {
|
||||
"hashes": [
|
||||
"sha256:ea204242ea83790e1512c9d0d8255002a652a6f4986e93cee664f28955ba0c22",
|
||||
"sha256:ec0ef1ba23ef95b1620f5d481334413700fb33f45cd76d56a63f4b0b1d76976a"
|
||||
"sha256:6f4e4bc1a11151178520567b50320e5c32f8edb552139d93ea3e30613b886f56",
|
||||
"sha256:86c3925020c27d072cdae7b828aaa5d165c2032a629abbe3c3a1be1edae61c58"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"django-flat-responsive": {
|
||||
"hashes": [
|
||||
"sha256:451caa2700c541b52fb7ce2d34d3d8dee9e980cf29f5463bc8a8c6256a1a6474"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
@@ -157,22 +163,23 @@
|
||||
"sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a",
|
||||
"sha256:fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"factory-boy": {
|
||||
"hashes": [
|
||||
"sha256:bd5a096d0f102d79b6c78cef1c8c0b650f2e1a3ecba351c735c6d2df8dabd29c",
|
||||
"sha256:be2abc8092294e4097935a29b4e37f5b9ed3e4205e2e32df215c0315b625995e"
|
||||
"sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca",
|
||||
"sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.10.0"
|
||||
"version": "==2.11.1"
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:226d8fa67a8cf8b4007aab721f67639f130e9cfdc53a7095a2290ebb07a65c71",
|
||||
"sha256:48fed4b4a191e2b42ad20c14115f1c6d36d338b80192075d7573f0f42d7fb321"
|
||||
"sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628",
|
||||
"sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad"
|
||||
],
|
||||
"version": "==0.8.13"
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"filemagic": {
|
||||
"hashes": [
|
||||
@@ -181,36 +188,36 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.6"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
||||
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"fuzzywuzzy": {
|
||||
"hashes": [
|
||||
"sha256:3759bc6859daa0eecef8c82b45404bdac20c23f23136cf4c18b46b426bbc418f",
|
||||
"sha256:5b36957ccf836e700f4468324fa80ba208990385392e217be077d5cd738ae602"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==0.15.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
|
||||
"sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
|
||||
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
|
||||
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.7.1"
|
||||
"version": "==19.9.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
|
||||
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.6"
|
||||
"version": "==2.7"
|
||||
},
|
||||
"inotify-simple": {
|
||||
"hashes": [
|
||||
"sha256:fc2c10dd73278a1027d0663f2db51240af5946390f363a154361406ebdddd8dd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.8"
|
||||
},
|
||||
"langdetect": {
|
||||
"hashes": [
|
||||
@@ -219,124 +226,95 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.0.7"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea",
|
||||
"sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e",
|
||||
"sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"
|
||||
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
|
||||
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
|
||||
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
|
||||
],
|
||||
"version": "==4.1.0"
|
||||
"version": "==4.3.0"
|
||||
},
|
||||
"pdftotext": {
|
||||
"hashes": [
|
||||
"sha256:0b82a9fd255a3f2bf5c861cf9e3174d3c4223e1e441bb060c611dcb4e65c6cb8"
|
||||
"sha256:b7312302007e19fc784263a321b41682f01a582af84e14200cef53b3f4e69a50"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.2"
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:00633bc2ec40313f4daf351855e506d296ec3c553f21b66720d0f1225ca84c6f",
|
||||
"sha256:03514478db61b034fc5d38b9bf060f994e5916776e93f02e59732a8270069c61",
|
||||
"sha256:040144ba422216aecf7577484865ade90e1a475f867301c48bf9fbd7579efd76",
|
||||
"sha256:16246261ff22368e5e32ad74d5ef40403ab6895171a7fc6d34f6c17cfc0f1943",
|
||||
"sha256:1cb38df69362af35c14d4a50123b63c7ff18ec9a6d4d5da629a6f19d05e16ba8",
|
||||
"sha256:2400e122f7b21d9801798207e424cbe1f716cee7314cd0c8963fdb6fc564b5fb",
|
||||
"sha256:2ee6364b270b56a49e8b8a51488e847ab130adc1220c171bed6818c0d4742455",
|
||||
"sha256:3b4560c3891b05022c464b09121bd507c477505a4e19d703e1027a3a7c68d896",
|
||||
"sha256:41374a6afb3f44794410dab54a0d7175e6209a5a02d407119c81083f1a4c1841",
|
||||
"sha256:438a3faf5f702c8d0f80b9f9f9b8382cfa048ca6a0d64ef71b86b563b0ee0359",
|
||||
"sha256:472a124c640bde4d5468f6991c9fa7e30b723d84ac4195a77c6ab6aea30f2b9c",
|
||||
"sha256:4d32c8e3623a61d6e29ccd024066cd1ba556555abfb4cd714155020e00107e3f",
|
||||
"sha256:4d8077fd649ac40a5c4165f2c22fa2a4ad18c668e271ecb2f9d849d1017a9313",
|
||||
"sha256:62ec7ae98357fcd46002c110bb7cad15fce532776f0cbe7ca1d44c49b837d49d",
|
||||
"sha256:6c7cab6a05351cf61e469937c49dbf3cdf5ffb3eeac71f8d22dc9be3507598d8",
|
||||
"sha256:6eca36905444c4b91fe61f1b9933a47a30480738a1dd26501ff67d94fc2bc112",
|
||||
"sha256:74e2ebfd19c16c28ad43b8a28ff73b904ed382ea4875188838541751986e8c9a",
|
||||
"sha256:7673e7473a13107059377c96c563aa36f73184c29d2926882e0a0210b779a1e7",
|
||||
"sha256:81762cf5fca9a82b53b7b2d0e6b420e0f3b06167b97678c81d00470daa622d58",
|
||||
"sha256:8554bbeb4218d9cfb1917c69e6f2d2ad0be9b18a775d2162547edf992e1f5f1f",
|
||||
"sha256:9b66e968da9c4393f5795285528bc862c7b97b91251f31a08004a3c626d18114",
|
||||
"sha256:a00edb2dec0035e98ac3ec768086f0b06dfabb4ad308592ede364ef573692f55",
|
||||
"sha256:b48401752496757e95304a46213c3155bc911ac884bed2e9b275ce1c1df3e293",
|
||||
"sha256:b6cf18f9e653a8077522bb3aa753a776b117e3e0cc872c25811cfdf1459491c2",
|
||||
"sha256:bb8adab1877e9213385cbb1adc297ed8337e01872c42a30cfaa66ff8c422779c",
|
||||
"sha256:c8a4b39ba380b57a31a4b5449a9d257b1302d8bc4799767e645dcee25725efe1",
|
||||
"sha256:cee9bc75bff455d317b6947081df0824a8f118de2786dc3d74a3503fd631f4ef",
|
||||
"sha256:d0dc1313dff48af64517cbbd85e046d6b477fbe5e9d69712801f024dcb08c62b",
|
||||
"sha256:d5bf527ed83617edd1855a5c923eeeaf68bcb9ac0ceb28e3f19b575b3a424984",
|
||||
"sha256:df5863a21f91de5ecdf7d32a32f406dd9867ebb35d41033b8bd9607a21887599",
|
||||
"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"
|
||||
"sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a",
|
||||
"sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72",
|
||||
"sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574",
|
||||
"sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd",
|
||||
"sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2",
|
||||
"sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c",
|
||||
"sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273",
|
||||
"sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554",
|
||||
"sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612",
|
||||
"sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934",
|
||||
"sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786",
|
||||
"sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27",
|
||||
"sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b",
|
||||
"sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62",
|
||||
"sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9",
|
||||
"sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710",
|
||||
"sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3",
|
||||
"sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4",
|
||||
"sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b",
|
||||
"sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f",
|
||||
"sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e",
|
||||
"sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b",
|
||||
"sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a",
|
||||
"sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f",
|
||||
"sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea",
|
||||
"sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7",
|
||||
"sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4",
|
||||
"sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333",
|
||||
"sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109",
|
||||
"sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.1.0"
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:714306e9b9a7b24ee4c1e3ff6463d7f652cdd30f4693121b31572e2fe1fdaea3",
|
||||
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
|
||||
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
|
||||
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
|
||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==0.7.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
||||
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
||||
"sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
|
||||
"sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
|
||||
],
|
||||
"version": "==1.5.3"
|
||||
"markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*'",
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:1ec08a51c901dfe44921576ed6e4c1f5b7ecbad403f871397feedb5eb8e4fa14",
|
||||
"sha256:5ff2fbcbab997895ba9ead77e1b38b3ebc2e5c3b8a6194ef918666e4c790a00e",
|
||||
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
||||
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
||||
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
|
||||
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pyocr": {
|
||||
"hashes": [
|
||||
"sha256:9ee8b5f38dd966ca531115fc5fe4715f7fa8961a9f14cd5109c2d938c17a2043"
|
||||
"sha256:bdc4d43bf9b63c2a9a4b2c9a1a623a0e63c8e6600eede5dbe866b31f3a5f2207"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.1"
|
||||
"version": "==0.5.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c",
|
||||
"sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"
|
||||
"sha256:2d7c49e931316cc7d1638a3e5f54f5d7b4e5225972b3c9838f3584788d27f349",
|
||||
"sha256:ad0c7db7b5d4081631e0155f5c61b80ad76ce148551aaafe3a718d65a7508b18"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.0"
|
||||
"version": "==3.7.4"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -348,11 +326,11 @@
|
||||
},
|
||||
"pytest-django": {
|
||||
"hashes": [
|
||||
"sha256:534505e0261cc566279032d9d887f844235342806fd63a6925689670fa1b29d7",
|
||||
"sha256:7501942093db2250a32a4e36826edfc542347bb9b26c78ed0649cdcfd49e5789"
|
||||
"sha256:2d2e0a618d91c280d463e90bcbea9b4e417609157f611a79685b1c561c4c0836",
|
||||
"sha256:59683def396923b78d7e191a7086a48193f8d5db869ace79acb38f906522bc7b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.1"
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"pytest-env": {
|
||||
"hashes": [
|
||||
@@ -377,35 +355,35 @@
|
||||
},
|
||||
"pytest-xdist": {
|
||||
"hashes": [
|
||||
"sha256:be2662264b035920ba740ed6efb1c816a83c8a22253df7766d129f6a7bfdbd35",
|
||||
"sha256:e8f5744acc270b3e7d915bdb4d5f471670f049b6fbd163d4cbd52203b075d30f"
|
||||
"sha256:0875deac20f6d96597036bdf63970887a6f36d28289c2f6682faf652dfea687b",
|
||||
"sha256:28e25e79698b2662b648319d3971c0f9ae0e6500f88258ccb9b153c31110ba9b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.22.2"
|
||||
"version": "==1.23.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:3220490fb9741e2342e1cf29a503394fdac874bc39568288717ee67047ff29df",
|
||||
"sha256:9d8074be4c993fbe4947878ce593052f71dac82932a677d49194d8ce9778002e"
|
||||
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
|
||||
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.2"
|
||||
"version": "==2.7.3"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:4965ed170bf51c347a89820e8050655e9c25db3837db6602e906b6d850fad85c",
|
||||
"sha256:509736185257111613009974e666568a1b031b028b61b500ef1ab4ee780089d5"
|
||||
"sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77",
|
||||
"sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.8.2"
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"python-gnupg": {
|
||||
"hashes": [
|
||||
"sha256:38f18712b7cfdd0d769bc88a21e90138154b9be2cbffb1e7d28bc37ee73a1c47",
|
||||
"sha256:5a54a6dd25bf78d3758dd7a1864f4efd122f9ca9402101d90e3ec4483ceafb73"
|
||||
"sha256:2d158dfc6b54927752b945ebe57e6a0c45da27747fa3b9ae66eccc0d2147ac0d",
|
||||
"sha256:faa69bab58ed0936f0ccf96c99b92369b7a1819305d37dfe5c927d21a437a09d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.2"
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"python-levenshtein": {
|
||||
"hashes": [
|
||||
@@ -415,38 +393,38 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
|
||||
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
|
||||
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
|
||||
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2018.4"
|
||||
"version": "==2018.5"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:1b428a296531ea1642a7da48562746309c5c06471a97bd0c02dd6a82e9cecee8",
|
||||
"sha256:27d72bb42dffb32516c28d218bb054ce128afd3e18464f30837166346758af67",
|
||||
"sha256:32cf4743debee9ea12d3626ee21eae83052763740e04086304e7a74778bf58c9",
|
||||
"sha256:32f6408dbca35040bc65f9f4ae1444d5546411fde989cb71443a182dd643305e",
|
||||
"sha256:333687d9a44738c486735955993f83bd22061a416c48f5a5f9e765e90cf1b0c9",
|
||||
"sha256:35eeccf17af3b017a54d754e160af597036435c58eceae60f1dd1364ae1250c7",
|
||||
"sha256:361a1fd703a35580a4714ec28d85e29780081a4c399a99bbfb2aee695d72aedb",
|
||||
"sha256:494bed6396a20d3aa6376bdf2d3fbb1005b8f4339558d8ac7b53256755f80303",
|
||||
"sha256:5b9c0ddd5b4afa08c9074170a2ea9b34ea296e32aeea522faaaaeeeb2fe0af2e",
|
||||
"sha256:a50532f61b23d4ab9d216a6214f359dd05c911c1a1ad20986b6738a782926c1a",
|
||||
"sha256:a9243d7b359b72c681a2c32eaa7ace8d346b7e8ce09d172a683acf6853161d9c",
|
||||
"sha256:b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29",
|
||||
"sha256:be42a601aaaeb7a317f818490a39d153952a97c40c6e9beeb2a1103616405348",
|
||||
"sha256:eee4d94b1a626490fc8170ffd788883f8c641b576e11ba9b4a29c9f6623371e0",
|
||||
"sha256:f69d1201a4750f763971ea8364ed95ee888fc128968b39d38883a72a4d005895"
|
||||
"sha256:22d7ef8c2df344328a8a3c61edade2ee714e5de9360911d22a9213931c769faa",
|
||||
"sha256:3a699780c6b712c67dc23207b129ccc6a7e1270233f7aadead3ea3f83c893702",
|
||||
"sha256:42f460d349baebd5faec02a0c920988fb0300b24baf898d9c139886565b66b6c",
|
||||
"sha256:43bf3d79940cbdf19adda838d8b26b28b47bec793cda46590b5b25703742f440",
|
||||
"sha256:47d6c7f0588ef33464e00023067c4e7cce68e0d6a686a73c7ee15abfdad503d4",
|
||||
"sha256:5b879f59f25ed9b91bc8693a9a994014b431f224f492519ad0255ce6b54b83e5",
|
||||
"sha256:8ba0093c412900f636b0f826c597a0c3ea0e395344bc99894ddefe88b76c9c7e",
|
||||
"sha256:a4789254a1a0bd7a637036cce0b7ed72d8cc864e93f2e9cfd10ac00ae27bb7b0",
|
||||
"sha256:b73cea07117dca888b0c3671770b501bef19aac9c45c8ffdb5bea2cca2377b0a",
|
||||
"sha256:d3eb59fa3e5b5438438ec97acd9dc86f077428e020b015b43987e35bea68ef4c",
|
||||
"sha256:d51d232b4e2f106deaf286001f563947fee255bc5bd209a696f027e15cf0a1e7",
|
||||
"sha256:d59b03131a8e35061b47a8f186324a95eaf30d5f6ee9cc0637e7b87d29c7c9b5",
|
||||
"sha256:dd705df1b47470388fc4630e4df3cbbe7677e2ab80092a1c660cae630a307b2d",
|
||||
"sha256:e87fffa437a4b00afb17af785da9b01618425d6cd984c677639deb937037d8f2",
|
||||
"sha256:ed40e0474ab5ab228a8d133759d451b31d3ccdebaff698646e54aff82c3de4f8"
|
||||
],
|
||||
"version": "==2018.2.21"
|
||||
"version": "==2018.8.29"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
|
||||
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
|
||||
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
|
||||
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
|
||||
],
|
||||
"version": "==2.18.4"
|
||||
"version": "==2.19.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -476,13 +454,28 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
||||
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"version": "==1.22"
|
||||
"markers": "python_version >= '2.6' and python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*'",
|
||||
"version": "==1.23"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
|
||||
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
|
||||
],
|
||||
"version": "==0.7.11"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||
@@ -490,6 +483,20 @@
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
|
||||
@@ -497,13 +504,35 @@
|
||||
],
|
||||
"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:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18",
|
||||
"sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:85882f97d75122ff8cdfe129215a408085a26039527110c8d4a2b8a5e45b7639",
|
||||
"sha256:a6ac981381b3f5f604b37a293369963485200e3639fb0404fa76092383c10c41"
|
||||
"sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62",
|
||||
"sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.3.1"
|
||||
"version": "==6.5.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
@@ -514,25 +543,45 @@
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
"sha256:1972f694c6bc66a2fac8718299e2ab73011d653a6d8059790c3476d2353b99ad",
|
||||
"sha256:5861f6dc0c16e024cbb0044999f9cf8013b292c05f287df06d3d991a87a4eb89"
|
||||
"sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:62bd6bf7f04ab5c817704ff513ef175328676471bdef3629d4bdd46626f75551",
|
||||
"sha256:a75a304d7090d2c67bd298091c14ef9d3d560e3c53de1c239617889f61d1d307"
|
||||
"sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
|
||||
"sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
"version": "==0.3.1"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
"sha256:9783f4644a3ef8528a6f20374eeb434431a650c797ca6d8df0d81e30fffdfa24",
|
||||
"sha256:9f8eb3277716a01faafaba553d629d3d60a1a624c7cf45daa600d2148c30020c"
|
||||
"sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba",
|
||||
"sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"
|
||||
],
|
||||
"markers": "sys_platform != 'win32'",
|
||||
"version": "==4.5.0"
|
||||
"version": "==4.6.0"
|
||||
},
|
||||
"pickleshare": {
|
||||
"hashes": [
|
||||
@@ -551,10 +600,10 @@
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
"sha256:e64193f0047ad603b71f202332ab5527c5e52aa7c8b609704fc28c0dc20c4365",
|
||||
"sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a"
|
||||
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
|
||||
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
|
||||
],
|
||||
"version": "==0.5.2"
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
@@ -563,6 +612,28 @@
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"
|
||||
@@ -576,6 +647,29 @@
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:a07050845cc9a2f4026a6035cc8ed795a5ce7be6528bbc82032385c10807dfe7",
|
||||
"sha256:d719de667218d763e8fd144b7fcfeefd8d434a6201f76bf9f0f0c1fa6f47fcdb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.8"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
||||
@@ -583,6 +677,14 @@
|
||||
],
|
||||
"version": "==4.3.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*'",
|
||||
"version": "==1.23"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
|
84
README-de.md
Normal file
84
README-de.md
Normal file
@@ -0,0 +1,84 @@
|
||||
*[English](README.md)*<br/>
|
||||
*[Greek](README-el.md)*
|
||||
|
||||
# Paperless
|
||||
|
||||
[](https://paperless.readthedocs.org/) [](https://gitter.im/danielquinn/paperless) [](https://travis-ci.org/danielquinn/paperless) [](https://coveralls.io/github/danielquinn/paperless?branch=master) [](https://github.com/danielquinn/paperless/blob/master/THANKS.md)
|
||||
|
||||
Indexiere und archiviere alle deine eingescannten Papierdokumente
|
||||
|
||||
Ich hasse Papier. Abgesehen von Umweltproblemen, ist es der Albtraum einer technisch-interessierten Person:
|
||||
|
||||
* Es gibt keine Suchfunktion
|
||||
* Es braucht physischen Platz
|
||||
* Sicherungen bedeuten mehr Papier
|
||||
|
||||
In den vergangenen Monaten hatte ich mehrmals das Problem, das richtige Dokument nicht zur Hand zu haben. Manchmal warf ich Dokumente weg, die ich noch gebraucht hätte (wer behält schon Wasserrechnungen für zwei Jahre?), andere verlor ich einfach... weil PAPIER. Ich schrieb dies, um mein Leben einfacher zu machen.
|
||||
|
||||
|
||||
|
||||
## Wie es funktioniert
|
||||
|
||||
Paperless steuert nicht deinen Scanner, es hilft nur damit umzugehen, was der Scanner herausspuckt
|
||||
|
||||
1. Kaufe einen Dokumentenscanner, der an einen Ort in deinem Netzwerk schreiben kann. Wenn du Inspirationen brauchst, schau in die [Scannerempfehlungen](https://paperless.readthedocs.io/en/latest/scanners.html).
|
||||
2. Stelle "Scanne zu FTP" oder ähnliches ein. Es sollte möglich sein, eingescannte Bilder ohne etwas tun zu müssen an einen Server hochzuladen. Natürlich kannst du auch die einscannte Datei händisch hochladen, wenn der Scanner automatisches Hochladen nicht unterstützt. Paperless ist es egal, wie die Dokumente in seinen lokalen Konsumordner gelangen.
|
||||
3. Besitze einen Zielserver, lasse das Papierless-Konsumskript laufen, um die Datei mit OCR zu versehen und sie in einer lokalen Datenbank zu indexieren.
|
||||
4. Benutze die Weboberfläche, um die Datenbank zu durchforsten und zu finden, was du suchst.
|
||||
5. Lade die PDF-Datei, die du brauchst/möchtest über die Weboberfläche herunter und mach was auch immer du willst damit. Du kannst es auch drucken und versenden, so als wäre es das Original. In den meisten Fällen, wird das niemanden interessieren oder bemerken.
|
||||
|
||||
Hier das, was du bekommt:
|
||||
|
||||

|
||||
|
||||
|
||||
## Dokumentation
|
||||
|
||||
Diese ist komplett verfügbar auf [ReadTheDocs](https://paperless.readthedocs.org/).
|
||||
|
||||
|
||||
## Anforderungen
|
||||
|
||||
Dies alles ist eine wirklich ziemlich einfache, glänzende und benutzerfreundliche Hülle rund um einige sehr mächtige Werkzeuge.
|
||||
|
||||
* [ImageMagick](http://imagemagick.org/) wandelt Bilder zwischen Farbe und Graustufen um.
|
||||
* [Tesseract](https://github.com/tesseract-ocr) erledigt die Buchstabenerkennung.
|
||||
* [Unpaper](https://www.flameeyes.eu/projects/unpaper) bereinigt und begradigt das eingescannte Bild.
|
||||
* [GNU Privacy Guard](https://gnupg.org/) wird als Verschlüsselungsbackend genutzt.
|
||||
* [Python 3](https://python.org/) ist die Sprache des Projekts.
|
||||
* [Pillow](https://pypi.python.org/pypi/pillowfight/) lädt die Bilddaten als Python-Objekt, um sie mit PyOCR zu verwenden.
|
||||
* [PyOCR](https://github.com/jflesch/pyocr) ist ein glatter, programmatischer Wrapper um Tesseract.
|
||||
* [Django](https://www.djangoproject.com/) ist das Framework, auf das dieses Projekt aufbaut.
|
||||
* [Python-GNUPG](http://pythonhosted.org/python-gnupg/) entschlüsselt die PDFs auf Abruf, um das Herunterladen unverschlüsselter Dateien zu ermöglichen, während die verschlüsselten Dateien auf der Festplatte bleiben.
|
||||
|
||||
|
||||
## Status des Projekts
|
||||
|
||||
Dieses Projekt wurde um 2015 gestartet und es gibt viele Leute, die es verwenden. Warum auch immer ist es ziemlich beliebt in Deutschland -- vielleicht kann jemand dort drüben mich über das Warum aufklären.
|
||||
|
||||
Ich entwickle keine neuen Funktionen mehr für Paperless, weil es genau das tut, was ich brauche und meine Aufmerksamkeit meinem neuesten Projekt [Aletheia](https://github.com/danielquinn/aletheia) gewidmet ist. Ich verlasse jedoch nicht das Projekt. Ich bin glücklich damit, Pull Requests zu begutachten und Fragen im Issue-Bereich zu beantworten. Wenn du ein Entwickler bist und eine neue Funktion willst, reihe sie in den Issues ein und/oder sende einen PR! Ich bin glücklich damit, neue Sachen hinzuzufügen, habe aber einfach nicht die Zeit, sie selbst zu erarbeiten.
|
||||
|
||||
|
||||
## Verknüpfte Prjekte
|
||||
|
||||
Paperless gibt es bereits seit einer Weile und Leute haben damit angefangen, Sachen rund um Paperless zu entwickeln. Wenn du einer dieser Menschen bist, kannst du dein Projekt zu dieser Liste hinzufügen:
|
||||
|
||||
* [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): Eine Desktop-Oberfläche für deine Paperless-Installation. Läuft auf Mac, Linux und Windows.
|
||||
* [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): Eine einfache Möglichkeit, Paperless via Ansible laufen zu lassen.
|
||||
|
||||
|
||||
## Ähnliche Projekte
|
||||
|
||||
Es gibt da draußen auch das Projekt [Mayan EDMS](https://mayan.readthedocs.org/en/latest/), welches überraschenderweise sehr große überschneidende Techniken hat wie Paperless. Mayan EDMS ist *viel* funktionsreicher und kommt ebenso mit einer glatten UI, aber kommt noch mit Python2; basiert jedoch auch auf Django und verwendet ein Konsummodell mit Tesseract und Unpaper. Es kann sein, dass Paperless weniger Ressourcen verbraucht, aber um ehrlich zu sein, hab ich das noch nicht selbst getestet. Eine Sache jedoch ist klar, *Paperless* ist ein **viel** besserer Name.
|
||||
|
||||
|
||||
## Wichtiger Hinweis
|
||||
|
||||
Dokumentenscanner werden typerweise verwendet, um sensible Dokumente zu scannen. Dinge wie die Sozialversicherungsnummer, Steueraufzeichnungen, Rechnungen, etc. Während Paperless die Originaldateien über das Konsumskript verschlüsselt, sind die OCR-Texte *nicht* verschlüsselt und demnach in Klartext gespeichert (es muss durchsuchbar sein, also wenn jemand eine Idee hat, wie man das mit verschlüsselten Daten tun kann: Ich bin ganz Ohr). Das bedeutet, dass Paperless niemals auf einem nicht vertrauten Host laufen sollte. Stattdessen empfehle ich, wenn du es verwenden willst, es lokal auf einem Server in deinem Zuhause laufen zu lassen.
|
||||
|
||||
|
||||
## Spenden
|
||||
|
||||
Wie mit aller Freier Software, liegt die Macht weniger in den Finanzen als mehr in den gemeinsamen Bemühungen. Ich schätze wirklich jeden Pull Request und Bugreport, der von Benutzern von Paperless getätigt wird, also bitte macht damit weiter. Wenn du jedoch nicht einer für Programmieren/Design/Dokumentation bist und mich wirklich finanziell unterstützen willst, sage ich nicht nein dazu ;-)
|
||||
|
||||
Das Ding ist, mir geht es finanziell OK, also würde ich dich darum bitten, an den [Hochkommissar der Vereinten Nationen für Flüchtlinge](https://donate.unhcr.org/int-en/general) zu spenden. Diese machen wichtige Arbeit und brauchen das Geld viel dringender als ich.
|
@@ -1,4 +1,5 @@
|
||||
*[English](README.md)*
|
||||
*[English](README.md)*<br/>
|
||||
*[German](README-de.md)*
|
||||
|
||||
# Paperless
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
*[German](README-de.md)*<br/>
|
||||
*[Greek](README-el.md)*
|
||||
|
||||
# Paperless
|
||||
|
@@ -1,8 +1,9 @@
|
||||
# Environment variables to set for Paperless
|
||||
# Commented out variables will be replaced by a default within Paperless.
|
||||
|
||||
# Passphrase Paperless uses to encrypt and decrypt your documents
|
||||
PAPERLESS_PASSPHRASE=CHANGE_ME
|
||||
# Passphrase Paperless uses to encrypt and decrypt your documents, if you want
|
||||
# encryption at all.
|
||||
# PAPERLESS_PASSPHRASE=CHANGE_ME
|
||||
|
||||
# The amount of threads to use for text recognition
|
||||
# PAPERLESS_OCR_THREADS=4
|
||||
|
@@ -1,6 +1,77 @@
|
||||
Changelog
|
||||
#########
|
||||
|
||||
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`_.
|
||||
|
||||
|
||||
2.1.0
|
||||
=====
|
||||
|
||||
* `Enno Lohmeier`_ added three simple features that make Paperless a lot more
|
||||
user (and developer) friendly:
|
||||
|
||||
1. There's a new search box on the front page: `#374`_.
|
||||
2. The correspondents & tags pages now have a column showing the number of
|
||||
relevant documents: `#375`_.
|
||||
3. The Dockerfile has been tweaked to build faster for those of us who are
|
||||
doing active development on Paperless using the Docker environment:
|
||||
`#376`_.
|
||||
|
||||
* You now also have the ability to customise the interface to your heart's
|
||||
content by creating a file called ``overrides.css`` and/or ``overrides.js``
|
||||
in the root of your media directory. Thanks to `Mark McFate`_ for this
|
||||
idea: `#371`_
|
||||
|
||||
|
||||
2.0.0
|
||||
=====
|
||||
|
||||
This is a big release as we've changed a core-functionality of Paperless: we no
|
||||
longer encrypt files with GPG by default.
|
||||
|
||||
The reasons for this are many, but it boils down to that the encryption wasn't
|
||||
really all that useful, as files on-disk were still accessible so long as you
|
||||
had the key, and the key was most typically stored in the config file. In
|
||||
other words, your files are only as safe as the ``paperless`` user is. In
|
||||
addition to that, *the contents of the documents were never encrypted*, so
|
||||
important numbers etc. were always accessible simply by querying the database.
|
||||
Still, it was better than nothing, but the consensus from users appears to be
|
||||
that it was more an annoyance than anything else, so this feature is now turned
|
||||
off unless you explicitly set a passphrase in your config file.
|
||||
|
||||
Migrating from 1.x
|
||||
------------------
|
||||
|
||||
Encryption isn't gone, it's just off for new users. So long as you have
|
||||
``PAPERLESS_PASSPHRASE`` set in your config or your environment, Paperless
|
||||
should continue to operate as it always has. If however, you want to drop
|
||||
encryption too, you only need to do two things:
|
||||
|
||||
1. Run ``./manage.py migrate && ./manage.py change_storage_type gpg unencrypted``.
|
||||
This will go through your entire database and Decrypt All The Things.
|
||||
2. Remove ``PAPERLESS_PASSPHRASE`` from your ``paperless.conf`` file, or simply
|
||||
stop declaring it in your environment.
|
||||
|
||||
Special thanks to `erikarvstedt`_, `matthewmoto`_, and `mcronce`_ who did the
|
||||
bulk of the work on this big change.
|
||||
|
||||
1.4.0
|
||||
=====
|
||||
|
||||
@@ -397,6 +468,14 @@ Changelog
|
||||
.. _erikarvstedt: https://github.com/erikarvstedt
|
||||
.. _Kyle Lucy: https://github.com/kmlucy
|
||||
.. _thinkjk: https://github.com/thinkjk
|
||||
.. _mcronce: https://github.com/mcronce
|
||||
.. _Enno Lohmeier: https://github.com/elohmeier
|
||||
.. _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
|
||||
|
||||
.. _#20: https://github.com/danielquinn/paperless/issues/20
|
||||
.. _#44: https://github.com/danielquinn/paperless/issues/44
|
||||
@@ -467,6 +546,15 @@ Changelog
|
||||
.. _#351: https://github.com/danielquinn/paperless/pull/351
|
||||
.. _#352: https://github.com/danielquinn/paperless/pull/352
|
||||
.. _#354: https://github.com/danielquinn/paperless/issues/354
|
||||
.. _#371: https://github.com/danielquinn/paperless/issues/371
|
||||
.. _#374: https://github.com/danielquinn/paperless/pull/374
|
||||
.. _#375: https://github.com/danielquinn/paperless/pull/375
|
||||
.. _#376: https://github.com/danielquinn/paperless/pull/376
|
||||
.. _#384: https://github.com/danielquinn/paperless/issues/384
|
||||
.. _#386: https://github.com/danielquinn/paperless/issues/386
|
||||
.. _#391: https://github.com/danielquinn/paperless/pull/391
|
||||
.. _#390: https://github.com/danielquinn/paperless/pull/390
|
||||
.. _#392: https://github.com/danielquinn/paperless/issues/392
|
||||
|
||||
.. _pipenv: https://docs.pipenv.org/
|
||||
.. _a new home on Docker Hub: https://hub.docker.com/r/danielquinn/paperless/
|
||||
|
@@ -17,7 +17,8 @@ The primary method of getting documents into your database is by putting them in
|
||||
the consumption directory. The ``document_consumer`` script runs in an infinite
|
||||
loop looking for new additions to this directory and when it finds them, it goes
|
||||
about the process of parsing them with the OCR, indexing what it finds, and
|
||||
encrypting the PDF, storing it in the media directory.
|
||||
encrypting the PDF (if ``PAPERLESS_PASSPHRASE`` is set), storing it in the
|
||||
media directory.
|
||||
|
||||
Getting stuff into this directory is up to you. If you're running Paperless
|
||||
on your local computer, you might just want to drag and drop files there, but if
|
||||
|
42
docs/customising.rst
Normal file
42
docs/customising.rst
Normal file
@@ -0,0 +1,42 @@
|
||||
.. _customising:
|
||||
|
||||
Customising Paperless
|
||||
#####################
|
||||
|
||||
Currently, the Paperless' interface is just the default Django admin, which
|
||||
while powerful, is rather boring. If you'd like to give the site a bit of a
|
||||
face-lift, or if you simply want to adjust the colours, contrast, or font size
|
||||
to make things easier to read, you can do that by adding your own CSS or
|
||||
Javascript quite easily.
|
||||
|
||||
|
||||
.. _customising-overrides:
|
||||
|
||||
Overrides
|
||||
=========
|
||||
|
||||
On every page load, Paperless looks for two files in your media root directory
|
||||
(the directory defined by your ``PAPERLESS_MEDIADIR`` configuration variable or
|
||||
the default, ``<project root>/media/``) for two files:
|
||||
|
||||
* ``overrides.css``
|
||||
* ``overrides.js``
|
||||
|
||||
If it finds either or both of those files, they'll be loaded into the page: the
|
||||
CSS in the ``<head>``, and the Javascript stuffed into the last line of the
|
||||
``<body>``.
|
||||
|
||||
|
||||
.. _customising-overrides-note:
|
||||
|
||||
An important note about customisation
|
||||
-------------------------------------
|
||||
|
||||
Any changes you make to the site with your CSS or Javascript are likely to
|
||||
depend on the structure of the current HTML and/or the existing CSS rules. For
|
||||
the most part it's safe to assume that these bits won't change, but *sometimes
|
||||
they do* as features are added or bugs are fixed.
|
||||
|
||||
If you make a change that you think others would appreciate though, submit it
|
||||
as a pull request and maybe we can find a way to work it into the project by
|
||||
default!
|
@@ -20,6 +20,8 @@ for you. This is is the logic the consumer follows:
|
||||
the pattern: ``Date - Correspondent - Title - tag,tag,tag.pdf``. Note that
|
||||
the format of the date is **rigidly defined** as ``YYYYMMDDHHMMSSZ`` or
|
||||
``YYYYMMDDZ``. The ``Z`` refers "Zulu time" AKA "UTC".
|
||||
The tags are optional, so the format ``Date - Correspondent - Title.pdf``
|
||||
works as well.
|
||||
2. If that doesn't work, we skip the date and try this pattern:
|
||||
``Correspondent - Title - tag,tag,tag.pdf``.
|
||||
3. If that doesn't work, we try to find the correspondent and title in the file
|
||||
|
@@ -40,6 +40,7 @@ Contents
|
||||
utilities
|
||||
guesswork
|
||||
migrating
|
||||
customising
|
||||
extending
|
||||
troubleshooting
|
||||
scanners
|
||||
|
@@ -16,7 +16,7 @@ Backing Up
|
||||
----------
|
||||
|
||||
So you're bored of this whole project, or you want to make a remote backup of
|
||||
the unencrypted files for whatever reason. This is easy to do, simply use the
|
||||
your files for whatever reason. This is easy to do, simply use the
|
||||
:ref:`exporter <utilities-exporter>` to dump your documents and database out
|
||||
into an arbitrary directory.
|
||||
|
||||
|
428
docs/setup.rst
428
docs/setup.rst
@@ -39,41 +39,47 @@ or just download the tarball and go that route:
|
||||
Installation & Configuration
|
||||
----------------------------
|
||||
|
||||
You can go multiple routes with setting up and running Paperless. The `Vagrant
|
||||
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
|
||||
Linux and in a VM with `Docker Machine`_ (this guide was written for native
|
||||
Docker usage under Linux, you might have to adapt it for Docker Machine.)
|
||||
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
|
||||
complicated, but worth it because it makes it easier should you want to
|
||||
contribute some code back.
|
||||
You can go multiple routes with setting up and running Paperless:
|
||||
|
||||
* The `bare metal route`_
|
||||
* The `vagrant route`_
|
||||
* The `docker route`_
|
||||
|
||||
|
||||
The `Vagrant route`_ is quick & easy, but means you're running a VM which comes
|
||||
with memory consumption, cpu overhead etc. The `docker route`_ offers the same
|
||||
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_
|
||||
.. _support Docker: setup-installation-docker_
|
||||
.. _bare metal: setup-installation-standard_
|
||||
.. _docker route: setup-installation-docker_
|
||||
.. _bare metal route: setup-installation-bare-metal_
|
||||
.. _Docker Machine: https://docs.docker.com/machine/
|
||||
|
||||
|
||||
.. _setup-installation-standard:
|
||||
.. _setup-installation-bare-metal:
|
||||
|
||||
Standard (Bare Metal)
|
||||
.....................
|
||||
+++++++++++++++++++++
|
||||
|
||||
1. Install the requirements as per the :ref:`requirements <requirements>` page.
|
||||
2. Within the extract of master.zip go to the ``src`` directory.
|
||||
3. Copy ``../paperless.conf.example`` to ``/etc/paperless.conf`` also the virtual
|
||||
envrionment look there for it and open it in your favourite editor.
|
||||
Because this file contains passwords it should only be readable by user root
|
||||
and paperless ! Set the values for:
|
||||
3. Copy ``../paperless.conf.example`` to ``/etc/paperless.conf`` and open it in
|
||||
your favourite editor. As this file contains passwords. It should only be
|
||||
readable by user root and paperless! Set the values for:
|
||||
|
||||
Set the values for:
|
||||
|
||||
* ``PAPERLESS_CONSUMPTION_DIR``: this is where your documents will be
|
||||
dumped to be consumed by Paperless.
|
||||
* ``PAPERLESS_PASSPHRASE``: this is the passphrase Paperless uses to
|
||||
encrypt/decrypt the original document.
|
||||
* ``PAPERLESS_OCR_THREADS``: this is the number of threads the OCR process
|
||||
will spawn to process document pages in parallel.
|
||||
* ``PAPERLESS_PASSPHRASE``: this is only required if you want to use GPG to
|
||||
encrypt your document files. This is the passphrase Paperless uses to
|
||||
encrypt/decrypt the original documents. Don't worry about defining this
|
||||
if you don't want to use encryption (the default).
|
||||
|
||||
4. Initialise the SQLite database with ``./manage.py migrate``.
|
||||
5. Create a user for your Paperless instance with
|
||||
@@ -81,9 +87,10 @@ Standard (Bare Metal)
|
||||
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``
|
||||
also known as http://localhost:8000/.
|
||||
You should now be able to visit your (empty) at `Paperless webserver`_ or
|
||||
whatever you chose before. You can login with the user/pass you created in
|
||||
#5.
|
||||
You should now be able to visit your (empty) installation at
|
||||
`Paperless webserver`_ or whatever you chose before. You can login with the
|
||||
user/pass you created in #5.
|
||||
|
||||
7. In a separate window, change to the ``src`` directory in this repo again,
|
||||
but this time, you should start the consumer script with
|
||||
``./manage.py document_consumer``.
|
||||
@@ -92,13 +99,18 @@ Standard (Bare Metal)
|
||||
10. Visit the document list on your webserver, and it should be there, indexed
|
||||
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:
|
||||
|
||||
Docker Method
|
||||
.............
|
||||
+++++++++++++
|
||||
|
||||
1. Install `Docker`_.
|
||||
|
||||
@@ -139,7 +151,8 @@ Docker Method
|
||||
|
||||
``PAPERLESS_PASSPHRASE``
|
||||
This is the passphrase Paperless uses to encrypt/decrypt the original
|
||||
document.
|
||||
document. If you aren't planning on using GPG encryption, you can just
|
||||
leave this undefined.
|
||||
|
||||
``PAPERLESS_OCR_THREADS``
|
||||
This is the number of threads the OCR process will spawn to process
|
||||
@@ -257,7 +270,7 @@ Docker Method
|
||||
.. _setup-installation-vagrant:
|
||||
|
||||
Vagrant Method
|
||||
..............
|
||||
++++++++++++++
|
||||
|
||||
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
|
||||
@@ -265,10 +278,11 @@ Vagrant Method
|
||||
3. Run ``vagrant ssh`` and once inside your new vagrant box, edit
|
||||
``/etc/paperless.conf`` and 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.
|
||||
* ``PAPERLESS_PASSPHRASE``: this is the passphrase Paperless uses to
|
||||
encrypt/decrypt the original document.
|
||||
* ``PAPERLESS_PASSPHRASE``: This is the passphrase Paperless uses to
|
||||
encrypt/decrypt the original document. It's only required if you want
|
||||
your original files to be encrypted, otherwise, just leave it unset.
|
||||
* ``PAPERLESS_EMAIL_SECRET``: this is the "magic word" used when consuming
|
||||
documents from mail or via the API. If you don't use either, leaving it
|
||||
blank is just fine.
|
||||
@@ -292,6 +306,11 @@ Vagrant Method
|
||||
11. Visit the document list on your webserver, and it should be there, indexed
|
||||
and downloadable.
|
||||
|
||||
.. caution::
|
||||
|
||||
This installation is not secure. Once everything is working head up to
|
||||
`Making things more permanent`_
|
||||
|
||||
.. _Vagrant: https://vagrantup.com/
|
||||
.. _Paperless server: http://172.28.128.4:8000
|
||||
|
||||
@@ -301,116 +320,39 @@ Vagrant Method
|
||||
Making Things a Little more Permanent
|
||||
-------------------------------------
|
||||
|
||||
Once you've tested things and are happy with the work flow, you can automate
|
||||
the process of starting the webserver and consumer automatically.
|
||||
Once you've tested things and are happy with the work flow, you should secure
|
||||
the installation and automate the process of starting the webserver and
|
||||
consumer.
|
||||
|
||||
|
||||
.. _setup-permanent-standard-systemd:
|
||||
|
||||
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:
|
||||
|
||||
.. _setup-permanent-webserver:
|
||||
|
||||
Using a Real Webserver
|
||||
......................
|
||||
++++++++++++++++++++++
|
||||
|
||||
The default is to use Django's development server, as that's easy and does the
|
||||
job well enough on a home network. However, if you want to do things right,
|
||||
it's probably a good idea to use a webserver capable of handling more than one
|
||||
thread. You will also have to let the webserver serve the static files (CSS,
|
||||
JavaScript) from the directory configured in ``PAPERLESS_STATICDIR``. For that,
|
||||
you need to run ``./manage.py collectstatic`` in the ``src`` directory. The
|
||||
default static files directory is ``../static``.
|
||||
job well enough on a home network. However it is heavily discouraged to use
|
||||
it for more than that.
|
||||
|
||||
If you want to do things right you should use a real webserver capable of
|
||||
handling more than one thread. You will also have to let the webserver serve
|
||||
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
|
||||
~~~~~~
|
||||
|
||||
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
|
||||
|
||||
@@ -441,170 +383,150 @@ Nginx + Gunicorn
|
||||
|
||||
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
|
||||
a copy of a simple Nginx configuration fragment making use of SSL and IPv6 to
|
||||
refer to a gunicorn instance listening on a local Unix socket:
|
||||
a copy of a simple Nginx configuration fragment making use of a gunicorn
|
||||
instance listening on localhost port 8000.
|
||||
|
||||
.. code:: nginx
|
||||
|
||||
upstream transfer_server {
|
||||
server unix:/run/example.com/gunicorn.sock fail_timeout=0;
|
||||
}
|
||||
|
||||
# Redirect requests on port 80 to 443
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.com;
|
||||
rewrite ^ https://$server_name$request_uri? permanent;
|
||||
listen 80;
|
||||
|
||||
index index.html index.htm index.php;
|
||||
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;
|
||||
listen [::]:443;
|
||||
client_max_body_size 4G;
|
||||
server_name example.com;
|
||||
keepalive_timeout 5;
|
||||
root /var/www/example.com;
|
||||
The gunicorn server can be started with the command:
|
||||
|
||||
ssl on;
|
||||
.. code-block:: shell
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||
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;
|
||||
$ <path-to-paperless-virtual-environment>/bin/gunicorn <path-to-paperless>/src/paperless.wsgi -w 2
|
||||
|
||||
# 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"
|
||||
# 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;
|
||||
.. _setup-permanent-standard-systemd:
|
||||
|
||||
add_header Strict-Transport-Security max-age=15768000;
|
||||
Standard (Bare Metal + Systemd)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
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.
|
||||
|
||||
access_log /var/log/nginx/example.com.log main;
|
||||
error_log /var/log/nginx/example.com.err info;
|
||||
1. You'll need to create a group and user called ``paperless`` (without login)
|
||||
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 / {
|
||||
try_files $uri @proxy_to_app;
|
||||
}
|
||||
.. code-block:: bash
|
||||
|
||||
location @proxy_to_app {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $host;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://transfer_server;
|
||||
}
|
||||
$ cp /path/to/paperless/scripts/paperless-consumer.service /etc/systemd/system/
|
||||
$ cp /path/to/paperless/scripts/paperless-webserver.service /etc/systemd/system/
|
||||
|
||||
}
|
||||
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
|
||||
your gunicorn instance. This should do the trick:
|
||||
.. code-block:: bash
|
||||
|
||||
.. 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'
|
||||
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
|
||||
.. _setup-permanent-standard-upstart:
|
||||
|
||||
def post_fork(server, worker):
|
||||
server.log.info("Worker spawned (pid: %s)", worker.pid)
|
||||
Standard (Bare Metal + Upstart)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
def pre_fork(server, worker):
|
||||
pass
|
||||
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:
|
||||
|
||||
def pre_exec(server):
|
||||
server.log.info("Forked child, re-executing.")
|
||||
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::
|
||||
|
||||
def when_ready(server):
|
||||
server.log.info("Server is ready. Spawning workers")
|
||||
start on (local-filesystems and net-device-up IFACE=eth0)
|
||||
stop on shutdown
|
||||
|
||||
def worker_int(worker):
|
||||
worker.log.info("worker received INT or QUIT signal")
|
||||
respawn
|
||||
respawn limit 10 5
|
||||
|
||||
## get traceback info
|
||||
import threading, sys, traceback
|
||||
id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
|
||||
code = []
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""),
|
||||
threadId))
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
code.append('File: "%s", line %d, in %s' % (filename,
|
||||
lineno, name))
|
||||
if line:
|
||||
code.append(" %s" % (line.strip()))
|
||||
worker.log.debug("\n".join(code))
|
||||
script
|
||||
exec <path to paperless virtual environment>/bin/gunicorn <path to parperless>/src/paperless.wsgi -w 2
|
||||
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 <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
|
||||
.......
|
||||
~~~~~~~
|
||||
|
||||
You may use the Ubuntu explanation above. Replace
|
||||
``(local-filesystems and net-device-up IFACE=eth0)`` with ``vagrant-mounted``.
|
||||
|
||||
|
||||
.. _setup-permanent-docker:
|
||||
|
||||
Docker
|
||||
......
|
||||
~~~~~~
|
||||
|
||||
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 daemon.
|
||||
|
||||
.. _restart-policy: https://docs.docker.com/engine/reference/commandline/run/#restart-policies-restart
|
||||
|
||||
|
||||
.. _setup-subdirectory:
|
||||
|
||||
Hosting Paperless in a Subdirectory
|
||||
-----------------------------------
|
||||
|
||||
Paperless was designed to run off the root of the hosting domain,
|
||||
(ie: ``https://example.com/``) but with a few changes, you can configure
|
||||
it to run in a subdirectory on your server
|
||||
(ie: ``https://example.com/paperless/``).
|
||||
|
||||
Thanks to the efforts of `maphy-psd`_ on `Github`_, running Paperless in a
|
||||
subdirectory is now as easy as setting a config variable. Simply set
|
||||
``PAPERLESS_FORCE_SCRIPT_NAME`` in your environment or
|
||||
``/etc/paperless.conf`` to the path you want Paperless hosted at, configure
|
||||
Nginx/Apache for your needs and you're done. So, if you want Paperless to live
|
||||
at ``https://example.com/arbitrary/path/to/paperless`` then you just set
|
||||
``PAPERLESS_FORCE_SCRIPT_NAME`` to ``/arbitrary/path/to/paperless``. Note the
|
||||
leading ``/`` there.
|
||||
|
||||
As to how to configure Nginx or Apache for this, that's on you :-)
|
||||
|
||||
.. _maphy-psd: https://github.com/maphy-psd
|
||||
.. _Github: https://github.com/danielquinn/paperless/pull/255
|
||||
|
@@ -59,8 +59,8 @@ for documents to parse and index. The process is pretty straightforward:
|
||||
4. Attempt to automatically assign document attributes by doing some guesswork.
|
||||
Read up on the :ref:`guesswork documentation<guesswork>` for more
|
||||
information about this process.
|
||||
5. Encrypt the document and store it in the ``media`` directory under
|
||||
``documents/originals``.
|
||||
5. Encrypt the document (if you have a passphrase set) and store it in the
|
||||
``media`` directory under ``documents/originals``.
|
||||
6. Go to #1.
|
||||
|
||||
|
||||
|
@@ -59,19 +59,19 @@ PAPERLESS_EMAIL_SECRET=""
|
||||
#### Security ####
|
||||
###############################################################################
|
||||
|
||||
# You must have a passphrase in order for Paperless to work at all. If you set
|
||||
# this to "", GNUGPG will "encrypt" your PDF by writing it out as a zero-byte
|
||||
# file.
|
||||
#
|
||||
# The passphrase you use here will be used when storing your documents in
|
||||
# Paperless, but you can always export them in an unencrypted format by using
|
||||
# document exporter. See the documentation for more information.
|
||||
# Paperless can be instructed to attempt to encrypt your PDF files with GPG
|
||||
# using the PAPERLESS_PASSPHRASE specified below. If however you're not
|
||||
# concerned about encrypting these files (for example if you have disk
|
||||
# encryption locally) then you don't need this and can safely leave this value
|
||||
# un-set.
|
||||
#
|
||||
# One final note about the passphrase. Once you've consumed a document with
|
||||
# one passphrase, DON'T CHANGE IT. Paperless assumes this to be a constant and
|
||||
# can't properly export documents that were encrypted with an old passphrase if
|
||||
# you've since changed it to a new one.
|
||||
PAPERLESS_PASSPHRASE="secret"
|
||||
#
|
||||
# The default is to not use encryption at all.
|
||||
#PAPERLESS_PASSPHRASE="secret"
|
||||
|
||||
|
||||
# The secret key has a default that should be fine so long as you're hosting
|
||||
@@ -89,6 +89,11 @@ PAPERLESS_PASSPHRASE="secret"
|
||||
# as is "example.com,www.example.com", but NOT " example.com" or "example.com,"
|
||||
#PAPERLESS_ALLOWED_HOSTS="example.com,www.example.com"
|
||||
|
||||
# If you decide to use Paperless APIs in an ajax calls, you need to add your
|
||||
# servers to the allowed hosts that can do CORS calls. By default Paperless allows
|
||||
# calls from localhost:8080. The same rules as above how the list should look like.
|
||||
#PAPERLESS_CORS_ALLOWED_HOSTS="localhost:8080,example.com,localhost:8000"
|
||||
|
||||
# To host paperless under a subpath url like example.com/paperless you set
|
||||
# this value to /paperless. No trailing slash!
|
||||
#
|
||||
|
@@ -1,52 +1,51 @@
|
||||
apipkg==1.4
|
||||
attrs==18.1.0
|
||||
certifi==2018.4.16
|
||||
-i https://pypi.python.org/simple
|
||||
apipkg==1.5; python_version != '3.1.*'
|
||||
atomicwrites==1.2.1; python_version != '3.1.*'
|
||||
attrs==18.2.0
|
||||
certifi==2018.8.24
|
||||
chardet==3.0.4
|
||||
coverage==4.5.1
|
||||
coveralls==1.3.0
|
||||
coverage==4.5.1; python_version != '3.1.*'
|
||||
coveralls==1.5.0
|
||||
dateparser==0.7.0
|
||||
django-cors-headers==2.4.0
|
||||
django-crispy-forms==1.7.2
|
||||
django-extensions==2.0.7
|
||||
django-filter==1.1.0
|
||||
django-flat-responsive==2.0
|
||||
django==1.11.13
|
||||
django-extensions==2.1.2
|
||||
django-filter==2.0.0
|
||||
django==2.0.8
|
||||
djangorestframework==3.8.2
|
||||
docopt==0.6.2
|
||||
execnet==1.5.0
|
||||
execnet==1.5.0; python_version != '3.1.*'
|
||||
factory-boy==2.11.1
|
||||
faker==0.8.15
|
||||
faker==0.9.0
|
||||
filemagic==1.6
|
||||
flake8==3.5.0
|
||||
fuzzywuzzy==0.15.0
|
||||
gunicorn==19.8.1
|
||||
idna==2.6
|
||||
inotify_simple==1.1.7; sys_platform == 'linux'
|
||||
gunicorn==19.9.0
|
||||
idna==2.7
|
||||
inotify-simple==1.1.8
|
||||
langdetect==1.0.7
|
||||
mccabe==0.6.1
|
||||
more-itertools==4.1.0
|
||||
pdftotext==2.0.2
|
||||
pillow==5.1.0
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
pycodestyle==2.3.1
|
||||
pyflakes==1.6.0
|
||||
pyocr==0.5.1
|
||||
more-itertools==4.3.0
|
||||
pdftotext==2.1.0
|
||||
pillow==5.2.0
|
||||
pluggy==0.7.1; python_version != '3.1.*'
|
||||
py==1.6.0; python_version != '3.1.*'
|
||||
pycodestyle==2.4.0
|
||||
pyocr==0.5.2
|
||||
pytest-cov==2.5.1
|
||||
pytest-django==3.2.1
|
||||
pytest-django==3.4.2
|
||||
pytest-env==0.6.2
|
||||
pytest-forked==0.2
|
||||
pytest-sugar==0.9.1
|
||||
pytest-xdist==1.22.2
|
||||
pytest==3.5.1
|
||||
pytest-xdist==1.23.0
|
||||
pytest==3.7.4
|
||||
python-dateutil==2.7.3
|
||||
python-dotenv==0.8.2
|
||||
python-gnupg==0.4.2
|
||||
python-dotenv==0.9.1
|
||||
python-gnupg==0.4.3
|
||||
python-levenshtein==0.12.0
|
||||
pytz==2018.4
|
||||
regex==2018.2.21
|
||||
requests==2.18.4
|
||||
pytz==2018.5
|
||||
regex==2018.8.29
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
termcolor==1.1.0
|
||||
text-unidecode==1.2
|
||||
tzlocal==1.5.1
|
||||
urllib3==1.22
|
||||
urllib3==1.23; python_version != '3.0.*'
|
||||
|
@@ -4,7 +4,7 @@ Description=Paperless webserver
|
||||
[Service]
|
||||
User=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]
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -0,0 +1 @@
|
||||
from .checks import changed_password_check
|
||||
|
@@ -3,8 +3,13 @@ from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
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.utils.safestring import mark_safe
|
||||
from django.utils.html import format_html, format_html_join
|
||||
|
||||
from .models import Correspondent, Tag, Document, Log
|
||||
|
||||
@@ -105,17 +110,24 @@ class CommonAdmin(admin.ModelAdmin):
|
||||
|
||||
class CorrespondentAdmin(CommonAdmin):
|
||||
|
||||
list_display = ("name", "match", "matching_algorithm")
|
||||
list_display = ("name", "match", "matching_algorithm", "document_count")
|
||||
list_filter = ("matching_algorithm",)
|
||||
list_editable = ("match", "matching_algorithm")
|
||||
|
||||
def document_count(self, obj):
|
||||
return obj.documents.count()
|
||||
|
||||
|
||||
class TagAdmin(CommonAdmin):
|
||||
|
||||
list_display = ("name", "colour", "match", "matching_algorithm")
|
||||
list_display = ("name", "colour", "match", "matching_algorithm",
|
||||
"document_count")
|
||||
list_filter = ("colour", "matching_algorithm")
|
||||
list_editable = ("colour", "match", "matching_algorithm")
|
||||
|
||||
def document_count(self, obj):
|
||||
return obj.documents.count()
|
||||
|
||||
|
||||
class DocumentAdmin(CommonAdmin):
|
||||
|
||||
@@ -130,6 +142,7 @@ class DocumentAdmin(CommonAdmin):
|
||||
"tags_")
|
||||
list_filter = ("tags", "correspondent", FinancialYearFilter,
|
||||
MonthListFilter)
|
||||
filter_horizontal = ("tags",)
|
||||
|
||||
ordering = ["-created", "correspondent"]
|
||||
|
||||
@@ -170,7 +183,7 @@ class DocumentAdmin(CommonAdmin):
|
||||
)
|
||||
}
|
||||
)
|
||||
return r
|
||||
return mark_safe(r)
|
||||
tags_.allow_tags = True
|
||||
|
||||
def document(self, obj):
|
||||
@@ -190,16 +203,13 @@ class DocumentAdmin(CommonAdmin):
|
||||
|
||||
@staticmethod
|
||||
def _html_tag(kind, inside=None, **kwargs):
|
||||
|
||||
attributes = []
|
||||
for lft, rgt in kwargs.items():
|
||||
attributes.append('{}="{}"'.format(lft, rgt))
|
||||
attributes = format_html_join(' ', '{}="{}"', kwargs.items())
|
||||
|
||||
if inside is not None:
|
||||
return "<{kind} {attributes}>{inside}</{kind}>".format(
|
||||
kind=kind, attributes=" ".join(attributes), inside=inside)
|
||||
return format_html("<{kind} {attributes}>{inside}</{kind}>",
|
||||
kind=kind, attributes=attributes, inside=inside)
|
||||
|
||||
return "<{} {}/>".format(kind, " ".join(attributes))
|
||||
return format_html("<{} {}/>", kind, attributes)
|
||||
|
||||
|
||||
class LogAdmin(CommonAdmin):
|
||||
|
39
src/documents/checks.py
Normal file
39
src/documents/checks.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import textwrap
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.checks import Error, register
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
|
||||
@register()
|
||||
def changed_password_check(app_configs, **kwargs):
|
||||
|
||||
from documents.models import Document
|
||||
from paperless.db import GnuPG
|
||||
|
||||
try:
|
||||
encrypted_doc = Document.objects.filter(
|
||||
storage_type=Document.STORAGE_TYPE_GPG).first()
|
||||
except OperationalError:
|
||||
return [] # No documents table yet
|
||||
|
||||
if encrypted_doc:
|
||||
|
||||
if not settings.PASSPHRASE:
|
||||
return [Error(
|
||||
"The database contains encrypted documents but no password "
|
||||
"is set."
|
||||
)]
|
||||
|
||||
if not GnuPG.decrypted(encrypted_doc.source_file):
|
||||
return [Error(textwrap.dedent(
|
||||
"""
|
||||
The current password doesn't match the password of the
|
||||
existing documents.
|
||||
|
||||
If you intend to change your password, you must first export
|
||||
all of the old documents, start fresh with the new password
|
||||
and then re-import them."
|
||||
"""))]
|
||||
|
||||
return []
|
@@ -29,7 +29,7 @@ class Consumer:
|
||||
Loop over every file found in CONSUMPTION_DIR and:
|
||||
1. Convert it to a greyscale pnm
|
||||
2. Use tesseract on the pnm
|
||||
3. Encrypt and store the document in the MEDIA_ROOT
|
||||
3. Store the document in the MEDIA_ROOT with optional encryption
|
||||
4. Store the OCR'd text in the database
|
||||
5. Delete the document and image(s)
|
||||
"""
|
||||
@@ -50,6 +50,10 @@ class Consumer:
|
||||
|
||||
os.makedirs(self.scratch, exist_ok=True)
|
||||
|
||||
self.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||
if settings.PASSPHRASE:
|
||||
self.storage_type = Document.STORAGE_TYPE_GPG
|
||||
|
||||
if not self.consume:
|
||||
raise ConsumerError(
|
||||
"The CONSUMPTION_DIR settings variable does not appear to be "
|
||||
@@ -213,7 +217,8 @@ class Consumer:
|
||||
file_type=file_info.extension,
|
||||
checksum=hashlib.md5(f.read()).hexdigest(),
|
||||
created=created,
|
||||
modified=created
|
||||
modified=created,
|
||||
storage_type=self.storage_type
|
||||
)
|
||||
|
||||
relevant_tags = set(list(Tag.match_all(text)) + list(file_info.tags))
|
||||
@@ -222,22 +227,22 @@ class Consumer:
|
||||
self.log("debug", "Tagging with {}".format(tag_names))
|
||||
document.tags.add(*relevant_tags)
|
||||
|
||||
# Encrypt and store the actual document
|
||||
with open(doc, "rb") as unencrypted:
|
||||
with open(document.source_path, "wb") as encrypted:
|
||||
self.log("debug", "Encrypting the document")
|
||||
encrypted.write(GnuPG.encrypted(unencrypted))
|
||||
|
||||
# Encrypt and store the thumbnail
|
||||
with open(thumbnail, "rb") as unencrypted:
|
||||
with open(document.thumbnail_path, "wb") as encrypted:
|
||||
self.log("debug", "Encrypting the thumbnail")
|
||||
encrypted.write(GnuPG.encrypted(unencrypted))
|
||||
self._write(document, doc, document.source_path)
|
||||
self._write(document, thumbnail, document.thumbnail_path)
|
||||
|
||||
self.log("info", "Completed")
|
||||
|
||||
return document
|
||||
|
||||
def _write(self, document, source, target):
|
||||
with open(source, "rb") as read_file:
|
||||
with open(target, "wb") as write_file:
|
||||
if document.storage_type == Document.STORAGE_TYPE_UNENCRYPTED:
|
||||
write_file.write(read_file.read())
|
||||
return
|
||||
self.log("debug", "Encrypting")
|
||||
write_file.write(GnuPG.encrypted(read_file))
|
||||
|
||||
def _cleanup_doc(self, doc):
|
||||
self.log("debug", "Deleting document {}".format(doc))
|
||||
os.unlink(doc)
|
||||
|
@@ -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
|
||||
|
||||
|
||||
class CorrespondentFilterSet(FilterSet):
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = Correspondent
|
||||
fields = {
|
||||
"name": [
|
||||
@@ -18,7 +18,7 @@ class CorrespondentFilterSet(FilterSet):
|
||||
|
||||
class TagFilterSet(FilterSet):
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = {
|
||||
"name": [
|
||||
@@ -42,12 +42,18 @@ class DocumentFilterSet(FilterSet):
|
||||
)
|
||||
}
|
||||
|
||||
correspondent__name = CharFilter(name="correspondent__name", **CHAR_KWARGS)
|
||||
correspondent__slug = CharFilter(name="correspondent__slug", **CHAR_KWARGS)
|
||||
tags__name = CharFilter(name="tags__name", **CHAR_KWARGS)
|
||||
tags__slug = CharFilter(name="tags__slug", **CHAR_KWARGS)
|
||||
correspondent__name = CharFilter(
|
||||
field_name="correspondent__name", **CHAR_KWARGS)
|
||||
correspondent__slug = CharFilter(
|
||||
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
|
||||
fields = {
|
||||
"title": [
|
||||
|
@@ -8,7 +8,6 @@ from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from .models import Document, Correspondent
|
||||
from .consumer import Consumer
|
||||
|
||||
|
||||
class UploadForm(forms.Form):
|
||||
|
119
src/documents/management/commands/change_storage_type.py
Normal file
119
src/documents/management/commands/change_storage_type.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from termcolor import colored as coloured
|
||||
|
||||
from documents.models import Document
|
||||
from paperless.db import GnuPG
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = (
|
||||
"This is how you migrate your stored documents from an encrypted "
|
||||
"state to an unencrypted one (or vice-versa)"
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
|
||||
parser.add_argument(
|
||||
"from",
|
||||
choices=("gpg", "unencrypted"),
|
||||
help="The state you want to change your documents from"
|
||||
)
|
||||
parser.add_argument(
|
||||
"to",
|
||||
choices=("gpg", "unencrypted"),
|
||||
help="The state you want to change your documents to"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--passphrase",
|
||||
help="If PAPERLESS_PASSPHRASE isn't set already, you need to "
|
||||
"specify it here"
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
try:
|
||||
print(coloured(
|
||||
"\n\nWARNING: This script is going to work directly on your "
|
||||
"document originals, so\nWARNING: you probably shouldn't run "
|
||||
"this unless you've got a recent backup\nWARNING: handy. It "
|
||||
"*should* work without a hitch, but be safe and backup your\n"
|
||||
"WARNING: stuff first.\n\nHit Ctrl+C to exit now, or Enter to "
|
||||
"continue.\n\n",
|
||||
"yellow",
|
||||
attrs=("bold",)
|
||||
))
|
||||
__ = input()
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
if options["from"] == options["to"]:
|
||||
raise CommandError(
|
||||
'The "from" and "to" values can\'t be the same.'
|
||||
)
|
||||
|
||||
passphrase = options["passphrase"] or settings.PASSPHRASE
|
||||
if not passphrase:
|
||||
raise CommandError(
|
||||
"Passphrase not defined. Please set it with --passphrase or "
|
||||
"by declaring it in your environment or your config."
|
||||
)
|
||||
|
||||
if options["from"] == "gpg" and options["to"] == "unencrypted":
|
||||
self.__gpg_to_unencrypted(passphrase)
|
||||
elif options["from"] == "unencrypted" and options["to"] == "gpg":
|
||||
self.__unencrypted_to_gpg(passphrase)
|
||||
|
||||
@staticmethod
|
||||
def __gpg_to_unencrypted(passphrase):
|
||||
|
||||
encrypted_files = Document.objects.filter(
|
||||
storage_type=Document.STORAGE_TYPE_GPG)
|
||||
|
||||
for document in encrypted_files:
|
||||
|
||||
print(coloured("Decrypting {}".format(document), "green"))
|
||||
|
||||
old_paths = [document.source_path, document.thumbnail_path]
|
||||
raw_document = GnuPG.decrypted(document.source_file, passphrase)
|
||||
raw_thumb = GnuPG.decrypted(document.thumbnail_file, passphrase)
|
||||
|
||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||
|
||||
with open(document.source_path, "wb") as f:
|
||||
f.write(raw_document)
|
||||
|
||||
with open(document.thumbnail_path, "wb") as f:
|
||||
f.write(raw_thumb)
|
||||
|
||||
document.save(update_fields=("storage_type",))
|
||||
|
||||
for path in old_paths:
|
||||
os.unlink(path)
|
||||
|
||||
@staticmethod
|
||||
def __unencrypted_to_gpg(passphrase):
|
||||
|
||||
unencrypted_files = Document.objects.filter(
|
||||
storage_type=Document.STORAGE_TYPE_UNENCRYPTED)
|
||||
|
||||
for document in unencrypted_files:
|
||||
|
||||
print(coloured("Encrypting {}".format(document), "green"))
|
||||
|
||||
old_paths = [document.source_path, document.thumbnail_path]
|
||||
with open(document.source_path, "rb") as raw_document:
|
||||
with open(document.thumbnail_path, "rb") as raw_thumb:
|
||||
document.storage_type = Document.STORAGE_TYPE_GPG
|
||||
with open(document.source_path, "wb") as f:
|
||||
f.write(GnuPG.encrypted(raw_document, passphrase))
|
||||
with open(document.thumbnail_path, "wb") as f:
|
||||
f.write(GnuPG.encrypted(raw_thumb, passphrase))
|
||||
|
||||
document.save(update_fields=("storage_type",))
|
||||
|
||||
for path in old_paths:
|
||||
os.unlink(path)
|
@@ -1,7 +1,5 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
@@ -13,7 +11,7 @@ from ...mail import MailFetcher, MailFetcherError
|
||||
try:
|
||||
from inotify_simple import INotify, flags
|
||||
except ImportError:
|
||||
pass
|
||||
INotify = flags = None
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -62,7 +60,8 @@ class Command(BaseCommand):
|
||||
parser.add_argument(
|
||||
"--no-inotify",
|
||||
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):
|
||||
@@ -71,8 +70,7 @@ class Command(BaseCommand):
|
||||
directory = options["directory"]
|
||||
loop_time = options["loop_time"]
|
||||
mail_delta = options["mail_delta"] * 60
|
||||
use_inotify = (not options["no_inotify"]
|
||||
and "inotify_simple" in sys.modules)
|
||||
use_inotify = INotify is not None and options["no_inotify"] is False
|
||||
|
||||
try:
|
||||
self.file_consumer = Consumer(consume=directory)
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core import serializers
|
||||
|
||||
@@ -45,9 +45,6 @@ class Command(Renderable, BaseCommand):
|
||||
if not os.access(self.target, os.W_OK):
|
||||
raise CommandError("That path doesn't appear to be writable")
|
||||
|
||||
if not settings.PASSPHRASE:
|
||||
settings.PASSPHRASE = input("Please enter the passphrase: ")
|
||||
|
||||
if options["legacy"]:
|
||||
self.dump_legacy()
|
||||
else:
|
||||
@@ -73,13 +70,20 @@ class Command(Renderable, BaseCommand):
|
||||
print("Exporting: {}".format(file_target))
|
||||
|
||||
t = int(time.mktime(document.created.timetuple()))
|
||||
with open(file_target, "wb") as f:
|
||||
f.write(GnuPG.decrypted(document.source_file))
|
||||
os.utime(file_target, times=(t, t))
|
||||
if document.storage_type == Document.STORAGE_TYPE_GPG:
|
||||
|
||||
with open(thumbnail_target, "wb") as f:
|
||||
f.write(GnuPG.decrypted(document.thumbnail_file))
|
||||
os.utime(thumbnail_target, times=(t, t))
|
||||
with open(file_target, "wb") as f:
|
||||
f.write(GnuPG.decrypted(document.source_file))
|
||||
os.utime(file_target, times=(t, t))
|
||||
|
||||
with open(thumbnail_target, "wb") as f:
|
||||
f.write(GnuPG.decrypted(document.thumbnail_file))
|
||||
os.utime(thumbnail_target, times=(t, t))
|
||||
|
||||
else:
|
||||
|
||||
shutil.copy(document.source_path, file_target)
|
||||
shutil.copy(document.thumbnail_path, thumbnail_target)
|
||||
|
||||
manifest += json.loads(
|
||||
serializers.serialize("json", Correspondent.objects.all()))
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
@@ -46,12 +47,6 @@ class Command(Renderable, BaseCommand):
|
||||
|
||||
self._check_manifest()
|
||||
|
||||
if not settings.PASSPHRASE:
|
||||
raise CommandError(
|
||||
"You need to define a passphrase before continuing. Please "
|
||||
"consult the documentation for setting up Paperless."
|
||||
)
|
||||
|
||||
# Fill up the database with whatever is in the manifest
|
||||
call_command("loaddata", manifest_path)
|
||||
|
||||
@@ -99,14 +94,21 @@ class Command(Renderable, BaseCommand):
|
||||
document_path = os.path.join(self.source, doc_file)
|
||||
thumbnail_path = os.path.join(self.source, thumb_file)
|
||||
|
||||
with open(document_path, "rb") as unencrypted:
|
||||
with open(document.source_path, "wb") as encrypted:
|
||||
print("Encrypting {} and saving it to {}".format(
|
||||
doc_file, document.source_path))
|
||||
encrypted.write(GnuPG.encrypted(unencrypted))
|
||||
if document.storage_type == Document.STORAGE_TYPE_GPG:
|
||||
|
||||
with open(thumbnail_path, "rb") as unencrypted:
|
||||
with open(document.thumbnail_path, "wb") as encrypted:
|
||||
print("Encrypting {} and saving it to {}".format(
|
||||
thumb_file, document.thumbnail_path))
|
||||
encrypted.write(GnuPG.encrypted(unencrypted))
|
||||
with open(document_path, "rb") as unencrypted:
|
||||
with open(document.source_path, "wb") as encrypted:
|
||||
print("Encrypting {} and saving it to {}".format(
|
||||
doc_file, document.source_path))
|
||||
encrypted.write(GnuPG.encrypted(unencrypted))
|
||||
|
||||
with open(thumbnail_path, "rb") as unencrypted:
|
||||
with open(document.thumbnail_path, "wb") as encrypted:
|
||||
print("Encrypting {} and saving it to {}".format(
|
||||
thumb_file, document.thumbnail_path))
|
||||
encrypted.write(GnuPG.encrypted(unencrypted))
|
||||
|
||||
else:
|
||||
|
||||
shutil.copy(document_path, document.source_path)
|
||||
shutil.copy(thumbnail_path, document.thumbnail_path)
|
||||
|
@@ -32,7 +32,6 @@ def realign_senders(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0002_auto_20151226_1316'),
|
||||
]
|
||||
|
@@ -6,7 +6,7 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
atomic = False
|
||||
dependencies = [
|
||||
('documents', '0010_log'),
|
||||
]
|
||||
|
@@ -112,7 +112,6 @@ def move_documents_and_create_thumbnails(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0011_auto_20160303_1929'),
|
||||
]
|
||||
|
@@ -128,7 +128,6 @@ def do_nothing(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0013_auto_20160325_2111'),
|
||||
]
|
||||
|
@@ -15,7 +15,6 @@ def reverse_func(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0018_auto_20170715_1712'),
|
||||
]
|
||||
|
@@ -11,8 +11,8 @@ def set_added_time_to_created_time(apps, schema_editor):
|
||||
doc.added = doc.created
|
||||
doc.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('documents', '0019_add_consumer_user'),
|
||||
]
|
||||
|
30
src/documents/migrations/0021_document_storage_type.py
Normal file
30
src/documents/migrations/0021_document_storage_type.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-04 13:07
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0020_document_added'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
# Add the field with the default GPG-encrypted value
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='storage_type',
|
||||
field=models.CharField(choices=[('unencrypted', 'Unencrypted'), ('gpg', 'Encrypted with GNU Privacy Guard')], default='gpg', editable=False, max_length=11),
|
||||
),
|
||||
|
||||
# Now that the field is added, change the default to unencrypted
|
||||
migrations.AlterField(
|
||||
model_name='document',
|
||||
name='storage_type',
|
||||
field=models.CharField(choices=[('unencrypted', 'Unencrypted'), ('gpg', 'Encrypted with GNU Privacy Guard')], default='unencrypted', editable=False, max_length=11),
|
||||
),
|
||||
|
||||
]
|
@@ -10,7 +10,10 @@ from collections import OrderedDict
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
try:
|
||||
from django.core.urlresolvers import reverse
|
||||
except ImportError:
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils import timezone
|
||||
@@ -57,8 +60,9 @@ class MatchingModel(models.Model):
|
||||
|
||||
is_insensitive = models.BooleanField(default=True)
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ("name",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -156,7 +160,7 @@ class Correspondent(MatchingModel):
|
||||
# better safe than sorry.
|
||||
SAFE_REGEX = re.compile(r"^[\w\- ,.']+$")
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@@ -190,6 +194,13 @@ class Document(models.Model):
|
||||
TYPE_TIF = "tiff"
|
||||
TYPES = (TYPE_PDF, TYPE_PNG, TYPE_JPG, TYPE_GIF, TYPE_TIF,)
|
||||
|
||||
STORAGE_TYPE_UNENCRYPTED = "unencrypted"
|
||||
STORAGE_TYPE_GPG = "gpg"
|
||||
STORAGE_TYPES = (
|
||||
(STORAGE_TYPE_UNENCRYPTED, "Unencrypted"),
|
||||
(STORAGE_TYPE_GPG, "Encrypted with GNU Privacy Guard")
|
||||
)
|
||||
|
||||
correspondent = models.ForeignKey(
|
||||
Correspondent,
|
||||
blank=True,
|
||||
@@ -229,10 +240,18 @@ class Document(models.Model):
|
||||
default=timezone.now, db_index=True)
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True, editable=False, db_index=True)
|
||||
|
||||
storage_type = models.CharField(
|
||||
max_length=11,
|
||||
choices=STORAGE_TYPES,
|
||||
default=STORAGE_TYPE_UNENCRYPTED,
|
||||
editable=False
|
||||
)
|
||||
|
||||
added = models.DateTimeField(
|
||||
default=timezone.now, editable=False, db_index=True)
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
ordering = ("correspondent", "title")
|
||||
|
||||
def __str__(self):
|
||||
@@ -246,11 +265,16 @@ class Document(models.Model):
|
||||
|
||||
@property
|
||||
def source_path(self):
|
||||
|
||||
file_name = "{:07}.{}".format(self.pk, self.file_type)
|
||||
if self.storage_type == self.STORAGE_TYPE_GPG:
|
||||
file_name += ".gpg"
|
||||
|
||||
return os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
"documents",
|
||||
"originals",
|
||||
"{:07}.{}.gpg".format(self.pk, self.file_type)
|
||||
file_name
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -267,11 +291,16 @@ class Document(models.Model):
|
||||
|
||||
@property
|
||||
def thumbnail_path(self):
|
||||
|
||||
file_name = "{:07}.png".format(self.pk)
|
||||
if self.storage_type == self.STORAGE_TYPE_GPG:
|
||||
file_name += ".gpg"
|
||||
|
||||
return os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
"documents",
|
||||
"thumbnails",
|
||||
"{:07}.png.gpg".format(self.pk)
|
||||
file_name
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -301,7 +330,7 @@ class Log(models.Model):
|
||||
|
||||
objects = LogManager()
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
ordering = ("-modified",)
|
||||
|
||||
def __str__(self):
|
||||
@@ -417,8 +446,10 @@ class FileInfo:
|
||||
def _get_tags(cls, tags):
|
||||
r = []
|
||||
for t in tags.split(","):
|
||||
r.append(
|
||||
Tag.objects.get_or_create(slug=t, defaults={"name": t})[0])
|
||||
r.append(Tag.objects.get_or_create(
|
||||
slug=t.lower(),
|
||||
defaults={"name": t}
|
||||
)[0])
|
||||
return tuple(r)
|
||||
|
||||
@classmethod
|
||||
|
@@ -5,14 +5,14 @@ from .models import Correspondent, Tag, Document, Log
|
||||
|
||||
class CorrespondentSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = Correspondent
|
||||
fields = ("id", "slug", "name")
|
||||
|
||||
|
||||
class TagSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = (
|
||||
"id", "slug", "name", "colour", "match", "matching_algorithm")
|
||||
@@ -34,7 +34,7 @@ class DocumentSerializer(serializers.ModelSerializer):
|
||||
view_name="drf:correspondent-detail", allow_null=True)
|
||||
tags = TagsField(view_name="drf:tag-detail", many=True)
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = Document
|
||||
fields = (
|
||||
"id",
|
||||
@@ -57,7 +57,7 @@ class LogSerializer(serializers.ModelSerializer):
|
||||
time = serializers.DateTimeField()
|
||||
messages = serializers.CharField()
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = Log
|
||||
fields = (
|
||||
"time",
|
||||
|
40
src/documents/templates/admin/base_site.html
Normal file
40
src/documents/templates/admin/base_site.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'admin/base_site.html' %}
|
||||
|
||||
{# NOTE: This should probably be extending base.html. See CSS comment below details. #}
|
||||
|
||||
|
||||
{% load custom_css from customisation %}
|
||||
{% load custom_js from customisation %}
|
||||
|
||||
|
||||
{% block blockbots %}
|
||||
|
||||
{% comment %}
|
||||
This really should be extending `extrastyle`, but the the
|
||||
django-flat-responsive package decided that it wanted to put its CSS in
|
||||
this block, so to make sure that overrides are in fact overriding
|
||||
everything else, we have to do the Wrong Thing here.
|
||||
|
||||
Once we switch to Django 2.x and drop django-flat-responsive, we should
|
||||
switch this to `extrastyle` where it should be.
|
||||
{% endcomment %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
{% custom_css %}
|
||||
|
||||
{% endblock blockbots %}
|
||||
|
||||
|
||||
{% block footer %}
|
||||
|
||||
{% comment %}
|
||||
The Django admin doesn't have a block for Javascript you'd want placed in
|
||||
the footer, so we have to use this one instead.
|
||||
{% endcomment %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
{% custom_js %}
|
||||
|
||||
{% endblock footer %}
|
@@ -4,6 +4,24 @@
|
||||
{% load i18n static %}
|
||||
|
||||
|
||||
{# This block adds a search form on the admin start page and on the module start page so that #}
|
||||
{# the user can quickly search for documents #}
|
||||
{% block pretitle %}
|
||||
<div>
|
||||
<h3>{% trans 'Search documents' %}</h3>
|
||||
|
||||
<div id="toolbar"><form id="changelist-search" method="get" action="{% url 'admin:documents_document_changelist' %}">
|
||||
<div><!-- DIV needed for valid HTML -->
|
||||
<label for="searchbar"><img src="{% static "admin/img/search.svg" %}" alt="Search"></label>
|
||||
<input type="text" size="40" name="q" value="" id="searchbar" autofocus="">
|
||||
<input type="submit" value="{% trans 'Search' %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{# This whole block is here just to override the `get_admin_log` line so #}
|
||||
{# that the log entries aren't limited to the current user #}
|
||||
{% block sidebar %}
|
||||
|
37
src/documents/templatetags/customisation.py
Normal file
37
src/documents/templatetags/customisation.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def custom_css():
|
||||
theme_path = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
"overrides.css"
|
||||
)
|
||||
if os.path.exists(theme_path):
|
||||
return mark_safe(
|
||||
'<link rel="stylesheet" type="text/css" href="{}" />'.format(
|
||||
os.path.join(settings.MEDIA_URL, "overrides.css")
|
||||
)
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def custom_js():
|
||||
theme_path = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
"overrides.js"
|
||||
)
|
||||
if os.path.exists(theme_path):
|
||||
return mark_safe(
|
||||
'<script src="{}"></script>'.format(
|
||||
os.path.join(settings.MEDIA_URL, "overrides.js")
|
||||
)
|
||||
)
|
||||
return ""
|
25
src/documents/tests/test_checks.py
Normal file
25
src/documents/tests/test_checks.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from ..checks import changed_password_check
|
||||
from ..models import Document
|
||||
from .factories import DocumentFactory
|
||||
|
||||
|
||||
class ChecksTestCase(TestCase):
|
||||
|
||||
def test_changed_password_check_empty_db(self):
|
||||
self.assertEqual(changed_password_check(None), [])
|
||||
|
||||
def test_changed_password_check_no_encryption(self):
|
||||
DocumentFactory.create(storage_type=Document.STORAGE_TYPE_UNENCRYPTED)
|
||||
self.assertEqual(changed_password_check(None), [])
|
||||
|
||||
@unittest.skip("I don't know how to test this")
|
||||
def test_changed_password_check_gpg_encryption_with_good_password(self):
|
||||
pass
|
||||
|
||||
@unittest.skip("I don't know how to test this")
|
||||
def test_changed_password_check_fail(self):
|
||||
pass
|
@@ -3,7 +3,7 @@ from unittest import mock
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from ..consumer import Consumer
|
||||
from ..models import FileInfo
|
||||
from ..models import FileInfo, Tag
|
||||
|
||||
|
||||
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):
|
||||
|
||||
|
@@ -52,12 +52,12 @@ class FetchView(SessionOrBasicAuthMixin, DetailView):
|
||||
|
||||
if self.kwargs["kind"] == "thumb":
|
||||
return HttpResponse(
|
||||
GnuPG.decrypted(self.object.thumbnail_file),
|
||||
self._get_raw_data(self.object.thumbnail_file),
|
||||
content_type=content_types[Document.TYPE_PNG]
|
||||
)
|
||||
|
||||
response = HttpResponse(
|
||||
GnuPG.decrypted(self.object.source_file),
|
||||
self._get_raw_data(self.object.source_file),
|
||||
content_type=content_types[self.object.file_type]
|
||||
)
|
||||
response["Content-Disposition"] = 'attachment; filename="{}"'.format(
|
||||
@@ -65,6 +65,11 @@ class FetchView(SessionOrBasicAuthMixin, DetailView):
|
||||
|
||||
return response
|
||||
|
||||
def _get_raw_data(self, file_handle):
|
||||
if self.object.storage_type == Document.STORAGE_TYPE_UNENCRYPTED:
|
||||
return file_handle
|
||||
return GnuPG.decrypted(file_handle)
|
||||
|
||||
|
||||
class PushView(SessionOrBasicAuthMixin, FormView):
|
||||
"""
|
||||
|
@@ -3,16 +3,9 @@ import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
# The runserver and consumer need to have access to the passphrase, so it
|
||||
# must be entered at start time to keep it safe.
|
||||
if "runserver" in sys.argv or "document_consumer" in sys.argv:
|
||||
if not settings.PASSPHRASE:
|
||||
settings.PASSPHRASE = input(
|
||||
"settings.PASSPHRASE is unset. Input passphrase: ")
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
@@ -2,7 +2,7 @@ import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.checks import Error, register, Warning
|
||||
from django.core.checks import Error, Warning, register
|
||||
|
||||
|
||||
@register()
|
||||
@@ -84,20 +84,3 @@ def binaries_check(app_configs, **kwargs):
|
||||
check_messages.append(Warning(error.format(binary), hint))
|
||||
|
||||
return check_messages
|
||||
|
||||
|
||||
@register()
|
||||
def config_check(app_configs, **kwargs):
|
||||
warning = (
|
||||
"It looks like you have PAPERLESS_SHARED_SECRET defined. Note that "
|
||||
"in the \npast, this variable was used for both API authentication "
|
||||
"and as the mail \nkeyword. As the API no no longer uses it, this "
|
||||
"variable has been renamed to \nPAPERLESS_EMAIL_SECRET, so if you're "
|
||||
"using the mail feature, you'd best update \nyour variable name.\n\n"
|
||||
"The old variable will stop working in a few months."
|
||||
)
|
||||
|
||||
if os.getenv("PAPERLESS_SHARED_SECRET"):
|
||||
return [Warning(warning)]
|
||||
|
||||
return []
|
||||
|
@@ -3,7 +3,7 @@ import gnupg
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class GnuPG(object):
|
||||
class GnuPG:
|
||||
"""
|
||||
A handy singleton to use when handling encrypted files.
|
||||
"""
|
||||
@@ -11,15 +11,22 @@ class GnuPG(object):
|
||||
gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME)
|
||||
|
||||
@classmethod
|
||||
def decrypted(cls, file_handle):
|
||||
return cls.gpg.decrypt_file(
|
||||
file_handle, passphrase=settings.PASSPHRASE).data
|
||||
def decrypted(cls, file_handle, passphrase=None):
|
||||
|
||||
if not passphrase:
|
||||
passphrase = settings.PASSPHRASE
|
||||
|
||||
return cls.gpg.decrypt_file(file_handle, passphrase=passphrase).data
|
||||
|
||||
@classmethod
|
||||
def encrypted(cls, file_handle):
|
||||
def encrypted(cls, file_handle, passphrase=None):
|
||||
|
||||
if not passphrase:
|
||||
passphrase = settings.PASSPHRASE
|
||||
|
||||
return cls.gpg.encrypt_file(
|
||||
file_handle,
|
||||
recipients=None,
|
||||
passphrase=settings.PASSPHRASE,
|
||||
passphrase=passphrase,
|
||||
symmetric=True
|
||||
).data
|
||||
|
@@ -2,7 +2,7 @@ from django.utils.deprecation import MiddlewareMixin
|
||||
from .models import User
|
||||
|
||||
|
||||
class Middleware (MiddlewareMixin):
|
||||
class Middleware(MiddlewareMixin):
|
||||
"""
|
||||
This is a dummy authentication middleware class that creates what
|
||||
is roughly an Anonymous authenticated user so we can disable login
|
||||
|
@@ -61,42 +61,42 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
|
||||
"corsheaders",
|
||||
"django_extensions",
|
||||
|
||||
"documents.apps.DocumentsConfig",
|
||||
"reminders.apps.RemindersConfig",
|
||||
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
||||
|
||||
"flat_responsive",
|
||||
"django.contrib.admin",
|
||||
|
||||
"rest_framework",
|
||||
"crispy_forms",
|
||||
"django_filters"
|
||||
"django_filters",
|
||||
|
||||
]
|
||||
|
||||
if os.getenv("PAPERLESS_INSTALLED_APPS"):
|
||||
INSTALLED_APPS += os.getenv("PAPERLESS_INSTALLED_APPS").split(",")
|
||||
|
||||
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'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 bool(os.getenv("PAPERLESS_DISABLE_LOGIN", "false").lower() in ("yes", "y", "1", "t", "true")):
|
||||
_index = MIDDLEWARE_CLASSES.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE_CLASSES[_index] = "paperless.middleware.Middleware"
|
||||
MIDDLEWARE_CLASSES.remove("django.contrib.auth.middleware.SessionAuthenticationMiddleware")
|
||||
_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE[_index] = "paperless.middleware.Middleware"
|
||||
|
||||
ROOT_URLCONF = 'paperless.urls'
|
||||
|
||||
@@ -221,12 +221,12 @@ OCR_LANGUAGE = os.getenv("PAPERLESS_OCR_LANGUAGE", "eng")
|
||||
OCR_THREADS = os.getenv("PAPERLESS_OCR_THREADS")
|
||||
|
||||
# OCR all documents?
|
||||
OCR_ALWAYS = bool(os.getenv("PAPERLESS_OCR_ALWAYS", "NO").lower() in ("yes", "y", "1", "t", "true"))
|
||||
OCR_ALWAYS = bool(os.getenv("PAPERLESS_OCR_ALWAYS", "NO").lower() in ("yes", "y", "1", "t", "true")) # NOQA
|
||||
|
||||
# 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
|
||||
# will simply be left in the CONSUMPTION_DIR.
|
||||
FORGIVING_OCR = bool(os.getenv("PAPERLESS_FORGIVING_OCR", "YES").lower() in ("yes", "y", "1", "t", "true"))
|
||||
FORGIVING_OCR = bool(os.getenv("PAPERLESS_FORGIVING_OCR", "YES").lower() in ("yes", "y", "1", "t", "true")) # NOQA
|
||||
|
||||
# GNUPG needs a home directory for some reason
|
||||
GNUPG_HOME = os.getenv("HOME", "/tmp")
|
||||
@@ -253,13 +253,17 @@ CONSUMPTION_DIR = os.getenv("PAPERLESS_CONSUMPTION_DIR")
|
||||
# slowly, you may want to use a higher value than the default.
|
||||
CONSUMER_LOOP_TIME = int(os.getenv("PAPERLESS_CONSUMER_LOOP_TIME", 10))
|
||||
|
||||
# This is used to encrypt the original documents and decrypt them later when
|
||||
# you want to download them. Set it and change the permissions on this file to
|
||||
# 0600, or set it to `None` and you'll be prompted for the passphrase at
|
||||
# runtime. The default looks for an environment variable.
|
||||
# DON'T FORGET TO SET THIS as leaving it blank may cause some strange things
|
||||
# with GPG, including an interesting case where it may "encrypt" zero-byte
|
||||
# files.
|
||||
# Pre-2.x versions of Paperless stored your documents locally with GPG
|
||||
# encryption, but that is no longer the default. This behaviour is still
|
||||
# available, but it must be explicitly enabled by setting
|
||||
# `PAPERLESS_PASSPHRASE` in your environment or config file. The default is to
|
||||
# store these files unencrypted.
|
||||
#
|
||||
# Translation:
|
||||
# * If you're a new user, you can safely ignore this setting.
|
||||
# * If you're upgrading from 1.x, this must be set, OR you can run
|
||||
# `./manage.py change_storage_type gpg unencrypted` to decrypt your files,
|
||||
# after which you can unset this value.
|
||||
PASSPHRASE = os.getenv("PAPERLESS_PASSPHRASE")
|
||||
|
||||
# Trigger a script after every successful document consumption?
|
||||
|
@@ -28,9 +28,11 @@ urlpatterns = [
|
||||
# API
|
||||
url(
|
||||
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
|
||||
url(
|
||||
|
@@ -1 +1 @@
|
||||
__version__ = (1, 3, 0)
|
||||
__version__ = (2, 2, 1)
|
||||
|
@@ -33,7 +33,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_2(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -43,7 +43,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_3(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -53,7 +53,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_4(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -66,7 +66,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_5(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -80,7 +80,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_6(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -100,7 +100,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_7(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -117,7 +117,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_8(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -138,7 +138,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_date_format_9(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "")
|
||||
@@ -153,7 +153,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_get_text_1_pdf(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "tests_date_1.pdf")
|
||||
@@ -359,7 +359,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_get_text_8_pdf(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "tests_date_8.pdf")
|
||||
@@ -373,7 +373,7 @@ class TestDate(TestCase):
|
||||
|
||||
@mock.patch(
|
||||
"paperless_tesseract.parsers.RasterisedDocumentParser.SCRATCH",
|
||||
SAMPLE_FILES
|
||||
SCRATCH
|
||||
)
|
||||
def test_get_text_9_pdf(self):
|
||||
input_file = os.path.join(self.SAMPLE_FILES, "tests_date_9.pdf")
|
||||
|
@@ -3,6 +3,8 @@ from django.db import models
|
||||
|
||||
class Reminder(models.Model):
|
||||
|
||||
document = models.ForeignKey("documents.Document")
|
||||
document = models.ForeignKey(
|
||||
"documents.Document", on_delete=models.PROTECT
|
||||
)
|
||||
date = models.DateTimeField()
|
||||
note = models.TextField(blank=True)
|
||||
|
Reference in New Issue
Block a user