mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-10 00:18:57 +00:00
Compare commits
351 Commits
ngx-1.6.0-
...
beta-1.6.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d8721c53c | ||
![]() |
aa67c851e3 | ||
![]() |
9737e4a24d | ||
![]() |
805c17565b | ||
![]() |
8e9d1cdd18 | ||
![]() |
1d3300fb34 | ||
![]() |
26d93cf3be | ||
![]() |
4e3183ee65 | ||
![]() |
8370ec58c1 | ||
![]() |
0d1bcd3c13 | ||
![]() |
a456b968af | ||
![]() |
3a242dc296 | ||
![]() |
5aff9b6fdb | ||
![]() |
fac6fe0c2e | ||
![]() |
efbc2a5715 | ||
![]() |
d18eebf97d | ||
![]() |
60cc7afc72 | ||
![]() |
b7949d2e69 | ||
![]() |
48175d5b8e | ||
![]() |
cb8fd6597d | ||
![]() |
70bc70ec97 | ||
![]() |
be2b59431f | ||
![]() |
c43193b17d | ||
![]() |
2147af0e6a | ||
![]() |
2666e70706 | ||
![]() |
19cf66be0f | ||
![]() |
8b26ddcd2c | ||
![]() |
47786dcb8c | ||
![]() |
5d91d6a885 | ||
![]() |
559f7c2683 | ||
![]() |
3b4da70c85 | ||
![]() |
95199bd325 | ||
![]() |
a8887b211e | ||
![]() |
9a758fc3dc | ||
![]() |
edc9c3f01c | ||
![]() |
83d769251d | ||
![]() |
bd66333147 | ||
![]() |
2228678520 | ||
![]() |
9a042118f9 | ||
![]() |
787ee454ff | ||
![]() |
32a4587bd3 | ||
![]() |
474050ba6b | ||
![]() |
34657fd675 | ||
![]() |
18747db17f | ||
![]() |
dccea9434a | ||
![]() |
40b07572a9 | ||
![]() |
9147e3a0bd | ||
![]() |
ed25212654 | ||
![]() |
ce5fe61e67 | ||
![]() |
9e9266b92a | ||
![]() |
e329d72e8b | ||
![]() |
92ec3fc060 | ||
![]() |
3afb3a905c | ||
![]() |
b3e6f04b30 | ||
![]() |
0a29f51862 | ||
![]() |
7149d407cd | ||
![]() |
fbb17df916 | ||
![]() |
34b317da7a | ||
![]() |
c161829803 | ||
![]() |
7c2ae129d7 | ||
![]() |
c49afa7caa | ||
![]() |
534c157809 | ||
![]() |
cae4d3fae3 | ||
![]() |
f59500c809 | ||
![]() |
b75bf255ba | ||
![]() |
1588242876 | ||
![]() |
00eff651e6 | ||
![]() |
a83d6a691a | ||
![]() |
5065f2cb80 | ||
![]() |
a4b36b041a | ||
![]() |
c360d9fa18 | ||
![]() |
78258eb9cb | ||
![]() |
71e5b5cf72 | ||
![]() |
4b0229584e | ||
![]() |
b2df8297e1 | ||
![]() |
95048b14fd | ||
![]() |
3360791a31 | ||
![]() |
4c65ecbe89 | ||
![]() |
65a56a1da0 | ||
![]() |
645f9b2ee2 | ||
![]() |
117157f02c | ||
![]() |
acab197a45 | ||
![]() |
feed7cd556 | ||
![]() |
c79b70fffd | ||
![]() |
42a5709202 | ||
![]() |
806f1ec0a3 | ||
![]() |
ccee85a05e | ||
![]() |
a27fb173dd | ||
![]() |
e4885badfc | ||
![]() |
62c488aff6 | ||
![]() |
afcb5fe3cf | ||
![]() |
d38bed1334 | ||
![]() |
f6a9d5b038 | ||
![]() |
863258f23d | ||
![]() |
3b76fa3f92 | ||
![]() |
023a42fa07 | ||
![]() |
537515432c | ||
![]() |
f93783052b | ||
![]() |
991bc1a1ce | ||
![]() |
1b5c557c44 | ||
![]() |
5794faef6c | ||
![]() |
ec01c436ea | ||
![]() |
9bb5568d8e | ||
![]() |
c25a107c04 | ||
![]() |
8f152aac69 | ||
![]() |
1cd5de697e | ||
![]() |
ce8c812669 | ||
![]() |
2c3ac053d8 | ||
![]() |
ca3bb6a540 | ||
![]() |
0932f095e1 | ||
![]() |
3d3807ce41 | ||
![]() |
30834eb8ff | ||
![]() |
1c4e74920a | ||
![]() |
e6efff426a | ||
![]() |
cacf60fdd9 | ||
![]() |
352f94ff2b | ||
![]() |
47530d274f | ||
![]() |
be13ec822f | ||
![]() |
c6da0cc9a2 | ||
![]() |
07623f9883 | ||
![]() |
6e0d334a0c | ||
![]() |
e514b99c57 | ||
![]() |
cffdaefe2f | ||
![]() |
9de4ca61e8 | ||
![]() |
d7919c45a3 | ||
![]() |
d24826a58e | ||
![]() |
3dfadcc397 | ||
![]() |
98d677dc0b | ||
![]() |
4f28225188 | ||
![]() |
8059039ef4 | ||
![]() |
682f3fdc3e | ||
![]() |
6f7aaba7fa | ||
![]() |
ea7a1012b9 | ||
![]() |
f24373699e | ||
![]() |
f74e15840d | ||
![]() |
c2c8a27545 | ||
![]() |
0979f3c5c3 | ||
![]() |
6f2fd1e2da | ||
![]() |
597effc856 | ||
![]() |
30b3510fbd | ||
![]() |
0cf9b11c3e | ||
![]() |
0388ce3e5b | ||
![]() |
1ea6a14437 | ||
![]() |
e8a073d538 | ||
![]() |
c8ac686b22 | ||
![]() |
81cfd13b4a | ||
![]() |
73a3abe535 | ||
![]() |
9006dd12f3 | ||
![]() |
2bcbb89175 | ||
![]() |
39daddef34 | ||
![]() |
ff4538612e | ||
![]() |
1782d00cc5 | ||
![]() |
d4f468208e | ||
![]() |
c60b708b9c | ||
![]() |
3dee012415 | ||
![]() |
a1d1fb962b | ||
![]() |
24e02a6c5f | ||
![]() |
4f352391ae | ||
![]() |
4667e0bf88 | ||
![]() |
9544e6c757 | ||
![]() |
83b5e1a49d | ||
![]() |
4f287b5ecd | ||
![]() |
4c346be367 | ||
![]() |
c87ac6f1ad | ||
![]() |
02dc395880 | ||
![]() |
63a3e0b325 | ||
![]() |
01ae5688d7 | ||
![]() |
ec8b10c85b | ||
![]() |
19ff8339be | ||
![]() |
a440b712de | ||
![]() |
3653045922 | ||
![]() |
0d7438e398 | ||
![]() |
81b17bec69 | ||
![]() |
e43c446c38 | ||
![]() |
0cb442c6e0 | ||
![]() |
17ea079fcd | ||
![]() |
2270aefdee | ||
![]() |
30828bcbe0 | ||
![]() |
4667adc64d | ||
![]() |
de779d453c | ||
![]() |
fe959b30c6 | ||
![]() |
3fcbb17a15 | ||
![]() |
cdfde1d91f | ||
![]() |
64a5b24e12 | ||
![]() |
7d29bd216d | ||
![]() |
1abd7cc2a0 | ||
![]() |
78608d92b4 | ||
![]() |
54cbacf4f4 | ||
![]() |
b43aae84ab | ||
![]() |
9d9789953b | ||
![]() |
614eb930d3 | ||
![]() |
62094a2098 | ||
![]() |
84ed805fd7 | ||
![]() |
82a158e803 | ||
![]() |
1527ae1472 | ||
![]() |
5d1e86fdc3 | ||
![]() |
a6d2a390f0 | ||
![]() |
a47ec7c77d | ||
![]() |
ee1404d99e | ||
![]() |
2caba558d7 | ||
![]() |
5e4119f6a9 | ||
![]() |
6b46b43367 | ||
![]() |
460f10f46a | ||
![]() |
37c6201a5a | ||
![]() |
4184988c0c | ||
![]() |
3264015dac | ||
![]() |
d1c785d1d0 | ||
![]() |
abd63df75b | ||
![]() |
57f35292d5 | ||
![]() |
7bd6f4a4ea | ||
![]() |
35de8e6ad5 | ||
![]() |
1549edfd55 | ||
![]() |
f33cf6cc2e | ||
![]() |
3a8cffe3ce | ||
![]() |
58066443de | ||
![]() |
20566b6571 | ||
![]() |
af52544792 | ||
![]() |
594445f6dd | ||
![]() |
c3e86f0f21 | ||
![]() |
d80102a7a4 | ||
![]() |
e886c38b45 | ||
![]() |
33a5823801 | ||
![]() |
b220d15fff | ||
![]() |
6fb856ee70 | ||
![]() |
3922588716 | ||
![]() |
940b12a908 | ||
![]() |
db5349881d | ||
![]() |
0edfe83a23 | ||
![]() |
7abc6440f6 | ||
![]() |
168ce2111d | ||
![]() |
617d0bfee7 | ||
![]() |
0d0da0d623 | ||
![]() |
dc657f2eb0 | ||
![]() |
ef2b4a7536 | ||
![]() |
e41d75c374 | ||
![]() |
eb55f5655f | ||
![]() |
9f72bf7745 | ||
![]() |
f34202a82a | ||
![]() |
f8c8161a3e | ||
![]() |
63aafd3133 | ||
![]() |
e2303235cd | ||
![]() |
1771d18a21 | ||
![]() |
d02a0b2213 | ||
![]() |
6cbbd0c515 | ||
![]() |
cae26f4f70 | ||
![]() |
f328d1461f | ||
![]() |
d3e9799279 | ||
![]() |
14eefe1f5d | ||
![]() |
6c2cc4cf50 | ||
![]() |
440f4729ac | ||
![]() |
e1c6042189 | ||
![]() |
55a057307c | ||
![]() |
8a5176e593 | ||
![]() |
a757778fba | ||
![]() |
1259911275 | ||
![]() |
4e4035a867 | ||
![]() |
9bd87e78dc | ||
![]() |
12a1c4ccbf | ||
![]() |
888e17cb91 | ||
![]() |
0fa48cea2a | ||
![]() |
9b7d393d5d | ||
![]() |
cc702cbdfa | ||
![]() |
e8ddb0c427 | ||
![]() |
feb677e656 | ||
![]() |
c19dd81ecf | ||
![]() |
01a50a3a98 | ||
![]() |
8920a32c75 | ||
![]() |
4773d0bb7f | ||
![]() |
20a6c5e7b7 | ||
![]() |
0826e0f96b | ||
![]() |
bd374f4c36 | ||
![]() |
bac8849f2d | ||
![]() |
ad87c3c87d | ||
![]() |
4c7eb34290 | ||
![]() |
a1fd9f7310 | ||
![]() |
9433908218 | ||
![]() |
8f1c4cd9c4 | ||
![]() |
85b210ebf6 | ||
![]() |
af7ffa3878 | ||
![]() |
96a84d16a6 | ||
![]() |
0737ea30e8 | ||
![]() |
1807811903 | ||
![]() |
a33dce1948 | ||
![]() |
ef4dc1b49e | ||
![]() |
0f886be109 | ||
![]() |
73a597855f | ||
![]() |
86d9c94962 | ||
![]() |
21ec2dfa68 | ||
![]() |
296d1d1b61 | ||
![]() |
af536dfefb | ||
![]() |
7d7c5207d7 | ||
![]() |
52f0a3dfb9 | ||
![]() |
4532bed7bc | ||
![]() |
d3bccc049b | ||
![]() |
333321e600 | ||
![]() |
081f11af40 | ||
![]() |
9f0f458a02 | ||
![]() |
e74c716588 | ||
![]() |
89bd0791dc | ||
![]() |
a591614a39 | ||
![]() |
0085c8351e | ||
![]() |
4df7f92a56 | ||
![]() |
1841cefbd8 | ||
![]() |
f13ae930a5 | ||
![]() |
dbe233f0ae | ||
![]() |
17e5f6d76b | ||
![]() |
f52d167da3 | ||
![]() |
63aa7128a7 | ||
![]() |
d30a652fc1 | ||
![]() |
1cd0684f62 | ||
![]() |
bc97a13a62 | ||
![]() |
db98eed392 | ||
![]() |
76115d6a81 | ||
![]() |
9e2fd52434 | ||
![]() |
7ca12c3dc4 | ||
![]() |
3dffa09977 | ||
![]() |
7c313eed33 | ||
![]() |
383cf7f4d5 | ||
![]() |
4babf0d102 | ||
![]() |
df4567f9e4 | ||
![]() |
db2bf66eee | ||
![]() |
36eaafa52a | ||
![]() |
f4d09d46f4 | ||
![]() |
01c1b42319 | ||
![]() |
472a3a54ab | ||
![]() |
9fbf9923dc | ||
![]() |
9d3242c13e | ||
![]() |
08c6a21bbb | ||
![]() |
7d18be9928 | ||
![]() |
748fe3b89f | ||
![]() |
b383e7dd08 | ||
![]() |
33ab69e010 | ||
![]() |
c52070d554 | ||
![]() |
1d67c32351 | ||
![]() |
23d6589a73 | ||
![]() |
3ae73e7df5 | ||
![]() |
8f3f60d249 | ||
![]() |
16664789d2 | ||
![]() |
f97aae1fd8 | ||
![]() |
7e1bbecc9f | ||
![]() |
376a56df26 | ||
![]() |
c41b86eb68 | ||
![]() |
e75ea257e8 | ||
![]() |
630cdaf08f | ||
![]() |
140b179607 | ||
![]() |
c8ad4e9bcd | ||
![]() |
49bc8f95f6 | ||
![]() |
50bab47f43 | ||
![]() |
785fcfb998 | ||
![]() |
c7541cb516 | ||
![]() |
7680bf7c0d |
@@ -17,3 +17,5 @@
|
|||||||
**/htmlcov
|
**/htmlcov
|
||||||
/src/.pytest_cache
|
/src/.pytest_cache
|
||||||
.idea
|
.idea
|
||||||
|
.venv/
|
||||||
|
.vscode/
|
||||||
|
@@ -18,14 +18,20 @@ max_line_length = off
|
|||||||
indent_size = 4
|
indent_size = 4
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
[*.yml]
|
[*.{yml,yaml}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
[*.rst]
|
[*.rst]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
# Tests don't get a line width restriction. It's still a good idea to follow
|
# Tests don't get a line width restriction. It's still a good idea to follow
|
||||||
# the 79 character rule, but in the interests of clarity, tests often need to
|
# the 79 character rule, but in the interests of clarity, tests often need to
|
||||||
# violate it.
|
# violate it.
|
||||||
[**/test_*.py]
|
[**/test_*.py]
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
|
|
||||||
|
[Dockerfile]
|
||||||
|
indent_style = space
|
||||||
|
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Something is not working
|
about: Something is not working
|
||||||
title: "[BUG] Concise description of the issue"
|
title: '[BUG] Concise description of the issue'
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!---
|
<!---
|
||||||
@@ -24,6 +23,7 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
@@ -36,13 +36,15 @@ A clear and concise description of what you expected to happen.
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Webserver logs**
|
**Webserver logs**
|
||||||
|
|
||||||
```
|
```
|
||||||
If available, post any logs from the web server related to your issue.
|
If available, post any logs from the web server related to your issue.
|
||||||
```
|
```
|
||||||
|
|
||||||
**Relevant information**
|
**Relevant information**
|
||||||
- Host OS of the machine running paperless: [e.g. Archlinux / Ubuntu 20.04]
|
|
||||||
- Browser [e.g. chrome, safari]
|
- Host OS of the machine running paperless: [e.g. Archlinux / Ubuntu 20.04]
|
||||||
- Version [e.g. 1.0.0]
|
- Browser [e.g. chrome, safari]
|
||||||
- Installation method: [docker / bare metal]
|
- Version [e.g. 1.0.0]
|
||||||
- Any configuration changes you made in `docker-compose.yml`, `docker-compose.env` or `paperless.conf`.
|
- Installation method: [docker / bare metal]
|
||||||
|
- Any configuration changes you made in `docker-compose.yml`, `docker-compose.env` or `paperless.conf`.
|
||||||
|
3
.github/ISSUE_TEMPLATE/other.md
vendored
3
.github/ISSUE_TEMPLATE/other.md
vendored
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: Other
|
name: Other
|
||||||
about: Anything that is not a feature request or bug.
|
about: Anything that is not a feature request or bug.
|
||||||
title: "[Other] Title of your issue"
|
title: '[Other] Title of your issue'
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
40
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
40
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!--
|
||||||
|
Note: All PRs with code changes should be targeted to the `dev` branch, pure documentation changes can target `main`
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Proposed change
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your poposed change can be tested. Screenshots and / or videos can also be helpful if appropriate.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please also tag the relevant team to help with review. You can tag any of the following:
|
||||||
|
@paperless-ngx/backend (Python / django, database, etc.)
|
||||||
|
@paperless-ngx/frontend (JavaScript/Typescript, HTML, CSS, etc.)
|
||||||
|
@paperless-ngx/ci-cd (GitHub Actions, deployment)
|
||||||
|
@paperless-ngx/test (General testing for larger PRs)
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!--
|
||||||
|
What type of change does your PR introduce to Paperless-ngx?
|
||||||
|
NOTE: Please check only one box!
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] Other (please explain)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
||||||
|
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||||
|
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://paperless-ngx.readthedocs.io/en/latest/extending.html#back-end-development).
|
||||||
|
- [ ] I have run all `pre-commit` hooks, see [documentation](https://paperless-ngx.readthedocs.io/en/latest/extending.html#code-formatting-with-pre-commit-hooks).
|
||||||
|
- [ ] I have made corresponding changes to the documentation as needed.
|
||||||
|
- [ ] I have checked my modifications for any breaking changes.
|
23
.github/dependabot.yml
vendored
23
.github/dependabot.yml
vendored
@@ -8,9 +8,12 @@ updates:
|
|||||||
target-branch: "dev"
|
target-branch: "dev"
|
||||||
# Look for `package.json` and `lock` files in the `root` directory
|
# Look for `package.json` and `lock` files in the `root` directory
|
||||||
directory: "/src-ui"
|
directory: "/src-ui"
|
||||||
# Check the npm registry for updates every week
|
# Check the npm registry for updates every month
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "monthly"
|
||||||
|
# Add reviewers
|
||||||
|
reviewers:
|
||||||
|
- "paperless-ngx/frontend"
|
||||||
|
|
||||||
# Enable version updates for Python
|
# Enable version updates for Python
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
@@ -20,3 +23,19 @@ updates:
|
|||||||
# Check for updates once a week
|
# Check for updates once a week
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
labels:
|
||||||
|
- "backend"
|
||||||
|
- "dependencies"
|
||||||
|
|
||||||
|
# Enable updates for Github Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
# Check for updates to GitHub Actions every month
|
||||||
|
interval: "monthly"
|
||||||
|
labels:
|
||||||
|
- "ci-cd"
|
||||||
|
- "dependencies"
|
||||||
|
# Add reviewers
|
||||||
|
reviewers:
|
||||||
|
- "paperless-ngx/backend"
|
||||||
|
234
.github/workflows/ci.yml
vendored
234
.github/workflows/ci.yml
vendored
@@ -13,90 +13,100 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
documentation:
|
documentation:
|
||||||
|
name: "Build Documentation"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
-
|
||||||
|
name: Install pipenv
|
||||||
|
run: pipx install pipenv
|
||||||
-
|
-
|
||||||
name: Set up Python
|
name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
-
|
cache: "pipenv"
|
||||||
name: Get pip cache dir
|
cache-dependency-path: 'Pipfile.lock'
|
||||||
id: pip-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(pip cache dir)"
|
|
||||||
-
|
|
||||||
name: Persistent Github pip cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-pip3.8}
|
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pipenv
|
pipenv sync --dev
|
||||||
pipenv install --system --dev --ignore-pipfile
|
|
||||||
-
|
-
|
||||||
name: Make documentation
|
name: Make documentation
|
||||||
run: |
|
run: |
|
||||||
cd docs/
|
cd docs/
|
||||||
make html
|
pipenv run make html
|
||||||
-
|
-
|
||||||
name: Upload artifact
|
name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: documentation
|
name: documentation
|
||||||
path: docs/_build/html/
|
path: docs/_build/html/
|
||||||
|
|
||||||
codestyle:
|
code-checks-backend:
|
||||||
|
name: "Backend Code Checks"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
-
|
-
|
||||||
name: Set up Python
|
name: Install checkers
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
-
|
|
||||||
name: Get pip cache dir
|
|
||||||
id: pip-cache
|
|
||||||
run: |
|
run: |
|
||||||
echo "::set-output name=dir::$(pip cache dir)"
|
pipx install reorder-python-imports
|
||||||
|
pipx install yesqa
|
||||||
|
pipx install add-trailing-comma
|
||||||
|
pipx install flake8
|
||||||
-
|
-
|
||||||
name: Persistent Github pip cache
|
name: Run reorder-python-imports
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-pip${{ matrix.python-version }}
|
|
||||||
-
|
|
||||||
name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pipenv
|
find src/ -type f -name '*.py' ! -path "*/migrations/*" | xargs reorder-python-imports
|
||||||
pipenv install --system --dev --ignore-pipfile
|
|
||||||
-
|
-
|
||||||
name: Codestyle
|
name: Run yesqa
|
||||||
run: |
|
run: |
|
||||||
cd src/
|
find src/ -type f -name '*.py' ! -path "*/migrations/*" | xargs yesqa
|
||||||
pycodestyle --max-line-length=88 --ignore=E121,E123,E126,E226,E24,E704,W503,W504,E203
|
|
||||||
codeformatting:
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Run add-trailing-comma
|
||||||
uses: actions/checkout@v2
|
run: |
|
||||||
|
find src/ -type f -name '*.py' ! -path "*/migrations/*" | xargs add-trailing-comma
|
||||||
|
# black is placed after add-trailing-comma because it may format differently
|
||||||
|
# if a trailing comma is added
|
||||||
-
|
-
|
||||||
name: Run black
|
name: Run black
|
||||||
uses: psf/black@stable
|
uses: psf/black@stable
|
||||||
with:
|
with:
|
||||||
options: "--check --diff"
|
options: "--check --diff"
|
||||||
version: "22.1.0"
|
version: "22.3.0"
|
||||||
|
-
|
||||||
|
name: Run flake8 checks
|
||||||
|
run: |
|
||||||
|
cd src/
|
||||||
|
flake8 --max-line-length=88 --ignore=E203,W503
|
||||||
|
|
||||||
tests:
|
code-checks-frontend:
|
||||||
|
name: "Frontend Code Checks"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
-
|
||||||
|
name: Install prettier
|
||||||
|
run: |
|
||||||
|
npm install prettier
|
||||||
|
-
|
||||||
|
name: Run prettier
|
||||||
|
run:
|
||||||
|
npx prettier --check --ignore-path Pipfile.lock **/*.js **/*.ts *.md **/*.md
|
||||||
|
|
||||||
|
tests-backend:
|
||||||
|
needs: [code-checks-backend]
|
||||||
|
name: "Backend Tests (${{ matrix.python-version }})"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -105,73 +115,93 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
-
|
||||||
|
name: Install pipenv
|
||||||
|
run: pipx install pipenv
|
||||||
-
|
-
|
||||||
name: Set up Python
|
name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
|
cache: "pipenv"
|
||||||
|
cache-dependency-path: 'Pipfile.lock'
|
||||||
-
|
-
|
||||||
name: Get pip cache dir
|
name: Install system dependencies
|
||||||
id: pip-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(pip cache dir)"
|
|
||||||
-
|
|
||||||
name: Persistent Github pip cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-pip${{ matrix.python-version }}
|
|
||||||
-
|
|
||||||
name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng
|
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng
|
||||||
pip install --upgrade pipenv
|
-
|
||||||
pipenv install --system --dev --ignore-pipfile
|
name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
pipenv sync --dev
|
||||||
-
|
-
|
||||||
name: Tests
|
name: Tests
|
||||||
run: |
|
run: |
|
||||||
cd src/
|
cd src/
|
||||||
pytest
|
pipenv run pytest
|
||||||
|
-
|
||||||
|
name: Get changed files
|
||||||
|
id: changed-files-specific
|
||||||
|
uses: tj-actions/changed-files@v18.1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
src/**
|
||||||
|
-
|
||||||
|
name: List all changed files
|
||||||
|
run: |
|
||||||
|
for file in ${{ steps.changed-files-specific.outputs.all_changed_files }}; do
|
||||||
|
echo "${file} was changed"
|
||||||
|
done
|
||||||
-
|
-
|
||||||
name: Publish coverage results
|
name: Publish coverage results
|
||||||
if: matrix.python-version == '3.9'
|
if: matrix.python-version == '3.9' && steps.changed-files-specific.outputs.any_changed == 'true'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# https://github.com/coveralls-clients/coveralls-python/issues/251
|
# https://github.com/coveralls-clients/coveralls-python/issues/251
|
||||||
run: |
|
run: |
|
||||||
cd src/
|
cd src/
|
||||||
coveralls --service=github
|
pipenv run coveralls --service=github
|
||||||
|
|
||||||
|
tests-frontend:
|
||||||
|
needs: [code-checks-frontend]
|
||||||
|
name: "Frontend Tests"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [16.x]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: cd src-ui && npm ci
|
||||||
|
- run: cd src-ui && npm run test
|
||||||
|
- run: cd src-ui && npm run e2e:ci
|
||||||
|
|
||||||
# build and push image to docker hub.
|
# build and push image to docker hub.
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || startsWith(github.ref, 'refs/tags/ngx-') || startsWith(github.ref, 'refs/tags/beta-'))
|
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || startsWith(github.ref, 'refs/tags/ngx-') || startsWith(github.ref, 'refs/tags/beta-'))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
needs: [tests, codeformatting, codestyle]
|
needs: [tests-backend, tests-frontend]
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Prepare
|
name: Gather Docker metadata
|
||||||
id: prepare
|
id: docker-meta
|
||||||
run: |
|
uses: docker/metadata-action@v3
|
||||||
IMAGE_NAME=ghcr.io/${{ github.repository }}
|
with:
|
||||||
if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then
|
images: ghcr.io/${{ github.repository }}
|
||||||
TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/tags/ngx-},${IMAGE_NAME}:latest
|
tags: |
|
||||||
INSPECT_TAG=${IMAGE_NAME}:latest
|
type=match,pattern=ngx-(\d.\d.\d),group=1
|
||||||
elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then
|
type=semver,pattern=ngx-{{version}}
|
||||||
TAGS=${IMAGE_NAME}:beta
|
type=semver,pattern=ngx-{{major}}.{{minor}}
|
||||||
INSPECT_TAG=${TAGS}
|
type=ref,event=branch
|
||||||
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
|
||||||
TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/heads/}
|
|
||||||
INSPECT_TAG=${TAGS}
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo ::set-output name=tags::${TAGS}
|
|
||||||
echo ::set-output name=inspect_tag::${INSPECT_TAG}
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
@@ -192,36 +222,37 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
push: true
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.prepare.outputs.tags }}
|
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
-
|
-
|
||||||
name: Inspect image
|
name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ steps.prepare.outputs.inspect_tag }}
|
docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
|
||||||
-
|
-
|
||||||
name: Export frontend artifact from docker
|
name: Export frontend artifact from docker
|
||||||
run: |
|
run: |
|
||||||
docker run -d --name frontend-extract ${{ steps.prepare.outputs.tags }}
|
docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
|
||||||
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
|
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
|
||||||
-
|
-
|
||||||
name: Upload frontend artifact
|
name: Upload frontend artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: frontend-compiled
|
name: frontend-compiled
|
||||||
path: src/documents/static/frontend/
|
path: src/documents/static/frontend/
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
needs: [build-docker-image, documentation, tests, codeformatting, codestyle]
|
needs: [build-docker-image, documentation]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
-
|
-
|
||||||
name: Set up Python
|
name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
-
|
-
|
||||||
@@ -233,13 +264,13 @@ jobs:
|
|||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
-
|
-
|
||||||
name: Download frontend artifact
|
name: Download frontend artifact
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: frontend-compiled
|
name: frontend-compiled
|
||||||
path: src/documents/static/frontend/
|
path: src/documents/static/frontend/
|
||||||
-
|
-
|
||||||
name: Download documentation artifact
|
name: Download documentation artifact
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: documentation
|
name: documentation
|
||||||
path: docs/_build/html/
|
path: docs/_build/html/
|
||||||
@@ -274,19 +305,19 @@ jobs:
|
|||||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||||
-
|
-
|
||||||
name: Upload release artifact
|
name: Upload release artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/paperless-ngx.tar.xz
|
path: dist/paperless-ngx.tar.xz
|
||||||
|
|
||||||
publish-release:
|
publish-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
needs: build-release
|
needs: build-release
|
||||||
if: contains(github.ref, 'refs/tags/ngx-') || contains(github.ref, 'refs/tags/beta-')
|
if: contains(github.ref, 'refs/tags/ngx-') || contains(github.ref, 'refs/tags/beta-')
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Download release artifact
|
name: Download release artifact
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: ./
|
path: ./
|
||||||
@@ -297,9 +328,11 @@ jobs:
|
|||||||
if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then
|
if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then
|
||||||
echo ::set-output name=version::${GITHUB_REF#refs/tags/ngx-}
|
echo ::set-output name=version::${GITHUB_REF#refs/tags/ngx-}
|
||||||
echo ::set-output name=prerelease::false
|
echo ::set-output name=prerelease::false
|
||||||
|
echo ::set-output name=body::"For a complete list of changes, see the changelog at https://paperless-ngx.readthedocs.io/en/latest/changelog.html"
|
||||||
elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then
|
elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then
|
||||||
echo ::set-output name=version::${GITHUB_REF#refs/tags/beta-}
|
echo ::set-output name=version::${GITHUB_REF#refs/tags/beta-}
|
||||||
echo ::set-output name=prerelease::true
|
echo ::set-output name=prerelease::true
|
||||||
|
echo ::set-output name=body::"For a complete list of changes, see the changelog at https://github.com/paperless-ngx/paperless-ngx/blob/beta/docs/changelog.rst"
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: Create release
|
name: Create release
|
||||||
@@ -312,8 +345,7 @@ jobs:
|
|||||||
release_name: Paperless-ngx ${{ steps.get_version.outputs.version }}
|
release_name: Paperless-ngx ${{ steps.get_version.outputs.version }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||||
body: |
|
body: ${{ steps.get_version.outputs.body }}
|
||||||
For a complete list of changes, see the changelog at https://paperless-ngx.readthedocs.io/en/latest/changelog.html.
|
|
||||||
-
|
-
|
||||||
name: Upload release archive
|
name: Upload release archive
|
||||||
id: upload-release-asset
|
id: upload-release-asset
|
||||||
|
47
.github/workflows/project-actions.yml
vendored
Normal file
47
.github/workflows/project-actions.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Project Automations
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
pull_request_target: #_target allows access to secrets
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
todo: Todo
|
||||||
|
done: Done
|
||||||
|
in_progress: In Progress
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
issue_opened_or_reopened:
|
||||||
|
name: issue_opened_or_reopened
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||||
|
steps:
|
||||||
|
- name: Set issue status to ${{ env.todo }}
|
||||||
|
uses: leonsteinhaeuser/project-beta-automations@v1.2.1
|
||||||
|
with:
|
||||||
|
gh_token: ${{ secrets.GH_TOKEN }}
|
||||||
|
organization: paperless-ngx
|
||||||
|
project_id: 2
|
||||||
|
resource_node_id: ${{ github.event.issue.node_id }}
|
||||||
|
status_value: ${{ env.todo }} # Target status
|
||||||
|
pr_opened_or_reopened:
|
||||||
|
name: pr_opened_or_reopened
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||||
|
steps:
|
||||||
|
- name: Set PR status to ${{ env.in_progress }}
|
||||||
|
uses: leonsteinhaeuser/project-beta-automations@v1.2.1
|
||||||
|
with:
|
||||||
|
gh_token: ${{ secrets.GH_TOKEN }}
|
||||||
|
organization: paperless-ngx
|
||||||
|
project_id: 2
|
||||||
|
resource_node_id: ${{ github.event.pull_request.node_id }}
|
||||||
|
status_value: ${{ env.in_progress }} # Target status
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -61,6 +61,9 @@ target/
|
|||||||
# PyCharm
|
# PyCharm
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Other stuff that doesn't belong
|
# Other stuff that doesn't belong
|
||||||
.virtualenv
|
.virtualenv
|
||||||
virtualenv
|
virtualenv
|
||||||
|
80
.pre-commit-config.yaml
Normal file
80
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# This file configures pre-commit hooks.
|
||||||
|
# See https://pre-commit.com/ for general information
|
||||||
|
# See https://pre-commit.com/hooks.html for a listing of possible hooks
|
||||||
|
|
||||||
|
repos:
|
||||||
|
# General hooks
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.1.0
|
||||||
|
hooks:
|
||||||
|
- id: check-docstring-first
|
||||||
|
- id: check-json
|
||||||
|
exclude: "tsconfig.*json"
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude_types:
|
||||||
|
- svg
|
||||||
|
- pofile
|
||||||
|
exclude: "(^LICENSE$)"
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args:
|
||||||
|
- "--fix=lf"
|
||||||
|
- id: trailing-whitespace
|
||||||
|
exclude_types:
|
||||||
|
- svg
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: detect-private-key
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: "v2.6.1"
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
types_or:
|
||||||
|
- javascript
|
||||||
|
- ts
|
||||||
|
- markdown
|
||||||
|
exclude: "(^Pipfile\\.lock$)"
|
||||||
|
# Python hooks
|
||||||
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
|
rev: v3.0.1
|
||||||
|
hooks:
|
||||||
|
- id: reorder-python-imports
|
||||||
|
exclude: "(migrations)"
|
||||||
|
- repo: https://github.com/asottile/yesqa
|
||||||
|
rev: "v1.3.0"
|
||||||
|
hooks:
|
||||||
|
- id: yesqa
|
||||||
|
exclude: "(migrations)"
|
||||||
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
|
rev: "v2.2.1"
|
||||||
|
hooks:
|
||||||
|
- id: add-trailing-comma
|
||||||
|
exclude: "(migrations)"
|
||||||
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
|
rev: 3.9.2
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
files: ^src/
|
||||||
|
args:
|
||||||
|
- "--config=./src/setup.cfg"
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.3.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
# Dockerfile hooks
|
||||||
|
- repo: https://github.com/pryorda/dockerfilelint-precommit-hooks
|
||||||
|
rev: "v0.1.0"
|
||||||
|
hooks:
|
||||||
|
- id: dockerfilelint
|
||||||
|
# Shell script hooks
|
||||||
|
- repo: https://github.com/lovesegfault/beautysh
|
||||||
|
rev: v6.2.1
|
||||||
|
hooks:
|
||||||
|
- id: beautysh
|
||||||
|
args:
|
||||||
|
- "--tab"
|
||||||
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
|
rev: "v0.8.0.4"
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# https://prettier.io/docs/en/options.html#semicolons
|
||||||
|
semi: false
|
||||||
|
# https://prettier.io/docs/en/options.html#quotes
|
||||||
|
singleQuote: true
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
hello@paperless-ngx.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
@@ -4,10 +4,10 @@ If you feel like contributing to the project, please do! Bug fixes and improveme
|
|||||||
|
|
||||||
If you want to implement something big:
|
If you want to implement something big:
|
||||||
|
|
||||||
* Please start a discussion about that in the issues! Maybe something similar is already in development and we can make it happen together.
|
- Please start a discussion about that in the issues! Maybe something similar is already in development and we can make it happen together.
|
||||||
* When making additions to the project, consider if the majority of users will benefit from your change. If not, you're probably better of forking the project.
|
- When making additions to the project, consider if the majority of users will benefit from your change. If not, you're probably better of forking the project.
|
||||||
* Also consider if your change will get in the way of other users. A good change is a change that enhances the experience of some users who want that change and does not affect users who do not care about the change.
|
- Also consider if your change will get in the way of other users. A good change is a change that enhances the experience of some users who want that change and does not affect users who do not care about the change.
|
||||||
* Please see the [paperless-ngx merge process](#merging-prs) below.
|
- Please see the [paperless-ngx merge process](#merging-prs) below.
|
||||||
|
|
||||||
## Python
|
## Python
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ Paperless supports python 3.8 and 3.9. We format Python code with [Black](https:
|
|||||||
|
|
||||||
## Branches
|
## Branches
|
||||||
|
|
||||||
`master` always reflects the latest release. Apart from changes to the documentation or readme, absolutely no functional changes on this branch in between releases.
|
`main` always reflects the latest release. Apart from changes to the documentation or readme, absolutely no functional changes on this branch in between releases.
|
||||||
|
|
||||||
`dev` contains all changes that will be part of the next release. Use this branch to start making your changes.
|
`dev` contains all changes that will be part of the next release. Use this branch to start making your changes.
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ Please format and test your code! I know it's a hassle, but it makes sure that y
|
|||||||
|
|
||||||
To test your code, execute `pytest` in the src/ directory. This also generates a html coverage report, which you can use to see if you missed anything important during testing.
|
To test your code, execute `pytest` in the src/ directory. This also generates a html coverage report, which you can use to see if you missed anything important during testing.
|
||||||
|
|
||||||
|
Before you can run `pytest`, ensure to [properly set up your local environment](https://paperless-ngx.readthedocs.io/en/latest/extending.html#initial-setup-and-first-start).
|
||||||
|
|
||||||
## More info:
|
## More info:
|
||||||
|
|
||||||
... is available in the documentation. https://paperless-ngx.readthedocs.io/en/latest/extending.html
|
... is available in the documentation. https://paperless-ngx.readthedocs.io/en/latest/extending.html
|
||||||
@@ -41,9 +43,9 @@ PRs deemed `non-trivial` will go through a stricter review process before being
|
|||||||
|
|
||||||
Examples of `non-trivial` PRs might include:
|
Examples of `non-trivial` PRs might include:
|
||||||
|
|
||||||
* Additional features
|
- Additional features
|
||||||
* Large changes to many distinct files
|
- Large changes to many distinct files
|
||||||
* Breaking or depreciation of existing features
|
- Breaking or depreciation of existing features
|
||||||
|
|
||||||
Our community review process for `non-trivial` PRs is the following:
|
Our community review process for `non-trivial` PRs is the following:
|
||||||
|
|
||||||
@@ -75,21 +77,56 @@ If a language has already been added, and you would like to contribute new trans
|
|||||||
If you would like the project to be translated to another language, first head over to https://crwd.in/paperless-ngx to check if that language has already been enabled for translation.
|
If you would like the project to be translated to another language, first head over to https://crwd.in/paperless-ngx to check if that language has already been enabled for translation.
|
||||||
If not, please request the language to be added by creating an issue on GitHub. The issue should contain:
|
If not, please request the language to be added by creating an issue on GitHub. The issue should contain:
|
||||||
|
|
||||||
* English name of the language (the localized name can be added on Crowdin).
|
- English name of the language (the localized name can be added on Crowdin).
|
||||||
* ISO language code. A list of those can be found here: https://support.crowdin.com/enterprise/language-codes/
|
- ISO language code. A list of those can be found here: https://support.crowdin.com/enterprise/language-codes/
|
||||||
* Date format commonly used for the language, e.g. dd/mm/yyyy, mm/dd/yyyy, etc.
|
- Date format commonly used for the language, e.g. dd/mm/yyyy, mm/dd/yyyy, etc.
|
||||||
|
|
||||||
After the language has been added and some translations have been made on Crowdin, the language needs to be enabled in the code.
|
After the language has been added and some translations have been made on Crowdin, the language needs to be enabled in the code.
|
||||||
Note that there is no need to manually add a .po of .xlf file as those will be automatically generated and imported from Crowdin.
|
Note that there is no need to manually add a .po of .xlf file as those will be automatically generated and imported from Crowdin.
|
||||||
The following files need to be changed:
|
The following files need to be changed:
|
||||||
|
|
||||||
* src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key)
|
- src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key)
|
||||||
* src/paperless/settings.py (in the _LANGUAGES_ array)
|
- src/paperless/settings.py (in the _LANGUAGES_ array)
|
||||||
* src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method)
|
- src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method)
|
||||||
* src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_)
|
- src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_)
|
||||||
|
|
||||||
Please add the language in the correct order, alphabetically by locale.
|
Please add the language in the correct order, alphabetically by locale.
|
||||||
Note that _en-us_ needs to stay on top of the list, as it is the default project language
|
Note that _en-us_ needs to stay on top of the list, as it is the default project language
|
||||||
|
|
||||||
If you are familiar with Git, feel free to send a Pull Request with those changes.
|
If you are familiar with Git, feel free to send a Pull Request with those changes.
|
||||||
If not, let us know in the issue you created for the language, so that another developer can make these changes.
|
If not, let us know in the issue you created for the language, so that another developer can make these changes.
|
||||||
|
|
||||||
|
# Organization Structure & Membership
|
||||||
|
|
||||||
|
Paperless-ngx is a community project. We do our best to delegate permission and responsibility among a team of people to ensure the longevity of the project.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
As of writing, there are 21 members in paperless-ngx. 4 of these people have complete administrative privileges to the repo:
|
||||||
|
|
||||||
|
- [@shamoon](https://github.com/shamoon)
|
||||||
|
- [@bauerj](https://github.com/bauerj)
|
||||||
|
- [@qcasey](https://github.com/qcasey)
|
||||||
|
- [@FrankStrieter](https://github.com/FrankStrieter)
|
||||||
|
|
||||||
|
There are 5 teams collaborating on specific tasks within paperless-ngx:
|
||||||
|
|
||||||
|
- @paperless-ngx/backend (Python / django)
|
||||||
|
- @paperless-ngx/frontend (JavaScript / Typescript)
|
||||||
|
- @paperless-ngx/ci-cd (GitHub Actions / Deployment)
|
||||||
|
- @paperless-ngx/issues (Issue triage)
|
||||||
|
- @paperless-ngx/test (General testing for larger PRs)
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
All team members are notified when mentioned or assigned to a relevant issue or pull request. Additionally, each team has slightly different access to paperless-ngx:
|
||||||
|
|
||||||
|
- The **test** team has no special permissions.
|
||||||
|
- The **issues** team has `triage` access. This means they can organize issues and pull requests.
|
||||||
|
- The **backend**, **frontend**, and **ci-cd** teams have `write` access. This means they can approve PRs and push code, containers, releases, and more.
|
||||||
|
|
||||||
|
## Joining
|
||||||
|
|
||||||
|
We are not overly strict with inviting people to the organization. If you have read the [team permissions](#permissions) and think having additional access would enhance your contributions, please reach out to an [admin](#structure) of the team.
|
||||||
|
|
||||||
|
The admins occasionally invite contributors directly if we believe having them on a team will accelerate their work.
|
||||||
|
140
Dockerfile
140
Dockerfile
@@ -3,64 +3,16 @@ FROM node:16 AS compile-frontend
|
|||||||
COPY . /src
|
COPY . /src
|
||||||
|
|
||||||
WORKDIR /src/src-ui
|
WORKDIR /src/src-ui
|
||||||
RUN npm update npm -g && npm install
|
RUN npm update npm -g && npm ci --no-optional
|
||||||
RUN ./node_modules/.bin/ng build --configuration production
|
RUN ./node_modules/.bin/ng build --configuration production
|
||||||
|
|
||||||
|
FROM ghcr.io/paperless-ngx/builder/ngx-base:1.0 as main-app
|
||||||
|
|
||||||
FROM ubuntu:20.04 AS jbig2enc
|
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
||||||
|
LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/"
|
||||||
WORKDIR /usr/src/jbig2enc
|
LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx"
|
||||||
|
LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx"
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends build-essential automake libtool libleptonica-dev zlib1g-dev git ca-certificates
|
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
||||||
|
|
||||||
RUN git clone https://github.com/agl/jbig2enc .
|
|
||||||
RUN ./autogen.sh
|
|
||||||
RUN ./configure && make
|
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.9-slim-bullseye
|
|
||||||
|
|
||||||
# Binary dependencies
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get -y --no-install-recommends install \
|
|
||||||
# Basic dependencies
|
|
||||||
curl \
|
|
||||||
gnupg \
|
|
||||||
imagemagick \
|
|
||||||
gettext \
|
|
||||||
tzdata \
|
|
||||||
gosu \
|
|
||||||
# fonts for text file thumbnail generation
|
|
||||||
fonts-liberation \
|
|
||||||
# for Numpy
|
|
||||||
libatlas-base-dev \
|
|
||||||
libxslt1-dev \
|
|
||||||
# thumbnail size reduction
|
|
||||||
optipng \
|
|
||||||
libxml2 \
|
|
||||||
pngquant \
|
|
||||||
unpaper \
|
|
||||||
zlib1g \
|
|
||||||
ghostscript \
|
|
||||||
icc-profiles-free \
|
|
||||||
# Mime type detection
|
|
||||||
file \
|
|
||||||
libmagic-dev \
|
|
||||||
media-types \
|
|
||||||
# OCRmyPDF dependencies
|
|
||||||
liblept5 \
|
|
||||||
tesseract-ocr \
|
|
||||||
tesseract-ocr-eng \
|
|
||||||
tesseract-ocr-deu \
|
|
||||||
tesseract-ocr-fra \
|
|
||||||
tesseract-ocr-ita \
|
|
||||||
tesseract-ocr-spa \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# copy jbig2enc
|
|
||||||
COPY --from=jbig2enc /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/
|
|
||||||
COPY --from=jbig2enc /usr/src/jbig2enc/src/jbig2 /usr/local/bin/
|
|
||||||
COPY --from=jbig2enc /usr/src/jbig2enc/src/*.h /usr/local/include/
|
|
||||||
|
|
||||||
WORKDIR /usr/src/paperless/src/
|
WORKDIR /usr/src/paperless/src/
|
||||||
|
|
||||||
@@ -68,47 +20,31 @@ COPY requirements.txt ../
|
|||||||
|
|
||||||
# Python dependencies
|
# Python dependencies
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
# python-Levenshtein still needs to be compiled here
|
||||||
&& apt-get -y --no-install-recommends install \
|
&& apt-get -y --no-install-recommends install \
|
||||||
build-essential \
|
build-essential \
|
||||||
libpq-dev \
|
&& python3 -m pip install --upgrade --no-cache-dir pip wheel \
|
||||||
git \
|
&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \
|
||||||
zlib1g-dev \
|
&& python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \
|
||||||
libjpeg62-turbo-dev \
|
&& apt-get -y purge build-essential \
|
||||||
&& if [ "$(uname -m)" = "armv7l" ] || [ "$(uname -m)" = "aarch64" ]; \
|
&& apt-get -y autoremove --purge \
|
||||||
then echo "Building qpdf" \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
&& mkdir -p /usr/src/qpdf \
|
|
||||||
&& cd /usr/src/qpdf \
|
|
||||||
&& git clone https://github.com/qpdf/qpdf.git . \
|
|
||||||
&& git checkout --quiet release-qpdf-10.6.2 \
|
|
||||||
&& ./configure \
|
|
||||||
&& make \
|
|
||||||
&& make install \
|
|
||||||
&& cd /usr/src/paperless/src/ \
|
|
||||||
&& rm -rf /usr/src/qpdf; \
|
|
||||||
else \
|
|
||||||
echo "Skipping qpdf build because pikepdf binary wheels are available."; \
|
|
||||||
fi \
|
|
||||||
&& python3 -m pip install --upgrade pip wheel \
|
|
||||||
&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \
|
|
||||||
&& python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \
|
|
||||||
&& apt-get -y purge build-essential git zlib1g-dev libjpeg62-turbo-dev \
|
|
||||||
&& apt-get -y autoremove --purge \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# setup docker-specific things
|
# setup docker-specific things
|
||||||
COPY docker/ ./docker/
|
COPY docker/ ./docker/
|
||||||
|
|
||||||
RUN cd docker \
|
RUN cd docker \
|
||||||
&& cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \
|
&& cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \
|
||||||
&& mkdir /var/log/supervisord /var/run/supervisord \
|
&& mkdir /var/log/supervisord /var/run/supervisord \
|
||||||
&& cp supervisord.conf /etc/supervisord.conf \
|
&& cp supervisord.conf /etc/supervisord.conf \
|
||||||
&& cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \
|
&& cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \
|
||||||
&& cp docker-prepare.sh /sbin/docker-prepare.sh \
|
&& chmod 755 /sbin/docker-entrypoint.sh \
|
||||||
&& chmod 755 /sbin/docker-entrypoint.sh \
|
&& cp docker-prepare.sh /sbin/docker-prepare.sh \
|
||||||
&& chmod +x install_management_commands.sh \
|
&& chmod 755 /sbin/docker-prepare.sh \
|
||||||
&& ./install_management_commands.sh \
|
&& chmod +x install_management_commands.sh \
|
||||||
&& cd .. \
|
&& ./install_management_commands.sh \
|
||||||
&& rm docker -rf
|
&& cd .. \
|
||||||
|
&& rm -rf docker/
|
||||||
|
|
||||||
COPY gunicorn.conf.py ../
|
COPY gunicorn.conf.py ../
|
||||||
|
|
||||||
@@ -117,18 +53,18 @@ COPY --from=compile-frontend /src/src/ ./
|
|||||||
|
|
||||||
# add users, setup scripts
|
# add users, setup scripts
|
||||||
RUN addgroup --gid 1000 paperless \
|
RUN addgroup --gid 1000 paperless \
|
||||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||||
&& chown -R paperless:paperless ../ \
|
&& chown -R paperless:paperless ../ \
|
||||||
&& gosu paperless python3 manage.py collectstatic --clear --no-input \
|
&& gosu paperless python3 manage.py collectstatic --clear --no-input \
|
||||||
&& gosu paperless python3 manage.py compilemessages
|
&& gosu paperless python3 manage.py compilemessages
|
||||||
|
|
||||||
|
VOLUME ["/usr/src/paperless/data", \
|
||||||
|
"/usr/src/paperless/media", \
|
||||||
|
"/usr/src/paperless/consume", \
|
||||||
|
"/usr/src/paperless/export"]
|
||||||
|
|
||||||
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
|
||||||
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||||
EXPOSE 8000
|
|
||||||
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
EXPOSE 8000
|
||||||
LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx"
|
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||||
LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx"
|
|
||||||
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
|
||||||
|
31
Pipfile
31
Pipfile
@@ -9,35 +9,36 @@ verify_ssl = true
|
|||||||
name = "piwheels"
|
name = "piwheels"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
dateparser = "~=1.1.0"
|
dateparser = "~=1.1"
|
||||||
django = "~=3.2"
|
django = "~=4.0"
|
||||||
django-cors-headers = "*"
|
django-cors-headers = "*"
|
||||||
django-extensions = "*"
|
django-extensions = "*"
|
||||||
django-filter = "~=21.1"
|
django-filter = "~=21.1"
|
||||||
django-q = "~=1.3.4"
|
django-q = "~=1.3"
|
||||||
djangorestframework = "~=3.13.1"
|
djangorestframework = "~=3.13"
|
||||||
filelock = "*"
|
filelock = "*"
|
||||||
fuzzywuzzy = {extras = ["speedup"], version = "*"}
|
fuzzywuzzy = {extras = ["speedup"], version = "*"}
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
imap-tools = "*"
|
imap-tools = "*"
|
||||||
langdetect = "*"
|
langdetect = "*"
|
||||||
numpy = "~=1.22.0"
|
|
||||||
pathvalidate = "*"
|
pathvalidate = "*"
|
||||||
pillow = "~=9.0"
|
pillow = "~=9.0"
|
||||||
pikepdf = "~=5.0"
|
# Any version update to pikepdf requires a base image update
|
||||||
|
pikepdf = "~=5.1"
|
||||||
python-gnupg = "*"
|
python-gnupg = "*"
|
||||||
python-dotenv = "*"
|
python-dotenv = "*"
|
||||||
python-dateutil = "*"
|
python-dateutil = "*"
|
||||||
python-magic = "*"
|
python-magic = "*"
|
||||||
psycopg2-binary = "*"
|
# Any version update to psycopg2 requires a base image update
|
||||||
|
psycopg2 = "*"
|
||||||
redis = "*"
|
redis = "*"
|
||||||
# Pinned because aarch64 wheels and updates cause warnings when loading the classifier model.
|
# Pinned because aarch64 wheels and updates cause warnings when loading the classifier model.
|
||||||
scikit-learn="==0.24.0"
|
scikit-learn="==1.0.2"
|
||||||
whitenoise = "~=6.0.0"
|
whitenoise = "~=6.0.0"
|
||||||
watchdog = "~=2.1.0"
|
watchdog = "~=2.1.0"
|
||||||
whoosh="~=2.7.4"
|
whoosh="~=2.7.4"
|
||||||
inotifyrecursive = "~=0.3.4"
|
inotifyrecursive = "~=0.3"
|
||||||
ocrmypdf = "~=13.4.0"
|
ocrmypdf = "~=13.4"
|
||||||
tqdm = "*"
|
tqdm = "*"
|
||||||
tika = "*"
|
tika = "*"
|
||||||
# TODO: This will sadly also install daphne+dependencies,
|
# TODO: This will sadly also install daphne+dependencies,
|
||||||
@@ -46,11 +47,10 @@ channels = "~=3.0"
|
|||||||
channels-redis = "*"
|
channels-redis = "*"
|
||||||
uvicorn = {extras = ["standard"], version = "*"}
|
uvicorn = {extras = ["standard"], version = "*"}
|
||||||
concurrent-log-handler = "*"
|
concurrent-log-handler = "*"
|
||||||
# uvloop 0.15+ incompatible with python 3.6
|
|
||||||
uvloop = "~=0.16"
|
|
||||||
cryptography = "~=36.0.1"
|
|
||||||
"pdfminer.six" = "*"
|
"pdfminer.six" = "*"
|
||||||
"backports.zoneinfo" = "*"
|
"backports.zoneinfo" = {version = "*", markers = "python_version < '3.9'"}
|
||||||
|
"importlib-resources" = {version = "*", markers = "python_version < '3.9'"}
|
||||||
|
zipp = {version = "*", markers = "python_version < '3.9'"}
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
@@ -62,7 +62,8 @@ pytest-django = "*"
|
|||||||
pytest-env = "*"
|
pytest-env = "*"
|
||||||
pytest-sugar = "*"
|
pytest-sugar = "*"
|
||||||
pytest-xdist = "*"
|
pytest-xdist = "*"
|
||||||
sphinx = "~=3.4.2"
|
sphinx = "~=4.4.0"
|
||||||
sphinx_rtd_theme = "*"
|
sphinx_rtd_theme = "*"
|
||||||
tox = "*"
|
tox = "*"
|
||||||
black = "*"
|
black = "*"
|
||||||
|
pre-commit = "*"
|
||||||
|
1035
Pipfile.lock
generated
1035
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
73
README.md
73
README.md
@@ -10,23 +10,23 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- omit in toc -->
|
<!-- omit in toc -->
|
||||||
|
|
||||||
# Paperless-ngx
|
# Paperless-ngx
|
||||||
|
|
||||||
Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, *less paper*.
|
Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, _less paper_.
|
||||||
|
|
||||||
Paperless-ngx forked from [paperless-ng](https://github.com/jonaswinkler/paperless-ng) to continue the great work and distribute responsibility of supporting and advancing the project among a team of people. [Consider joining us!](#community-support) Discussion of this transition can be found in issues
|
Paperless-ngx forked from [paperless-ng](https://github.com/jonaswinkler/paperless-ng) to continue the great work and distribute responsibility of supporting and advancing the project among a team of people. [Consider joining us!](#community-support) Discussion of this transition can be found in issues
|
||||||
[#1599](https://github.com/jonaswinkler/paperless-ng/issues/1599) and [#1632](https://github.com/jonaswinkler/paperless-ng/issues/1632).
|
[#1599](https://github.com/jonaswinkler/paperless-ng/issues/1599) and [#1632](https://github.com/jonaswinkler/paperless-ng/issues/1632).
|
||||||
|
|
||||||
A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. *Note: demo content is reset frequently and confidential information should not be uploaded.*
|
A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._
|
||||||
|
|
||||||
|
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Getting started](#getting-started)
|
- [Getting started](#getting-started)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [Community Support](#community-support)
|
- [Community Support](#community-support)
|
||||||
- [Translation](#translation)
|
- [Translation](#translation)
|
||||||
- [Feature Requests](#feature-requests)
|
- [Feature Requests](#feature-requests)
|
||||||
- [Bugs](#bugs)
|
- [Bugs](#bugs)
|
||||||
- [Affiliated Projects](#affiliated-projects)
|
- [Affiliated Projects](#affiliated-projects)
|
||||||
- [Important Note](#important-note)
|
- [Important Note](#important-note)
|
||||||
|
|
||||||
@@ -35,28 +35,28 @@ A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com)
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
* Organize and index your scanned documents with tags, correspondents, types, and more.
|
- Organize and index your scanned documents with tags, correspondents, types, and more.
|
||||||
* Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents.
|
- Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents.
|
||||||
* Supports PDF documents, images, plain text files, and Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents).
|
- Supports PDF documents, images, plain text files, and Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents).
|
||||||
* Office document support is optional and provided by Apache Tika (see [configuration](https://paperless-ngx.readthedocs.io/en/latest/configuration.html#tika-settings))
|
- Office document support is optional and provided by Apache Tika (see [configuration](https://paperless-ngx.readthedocs.io/en/latest/configuration.html#tika-settings))
|
||||||
* Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely.
|
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely.
|
||||||
* Single page application front end.
|
- Single page application front end.
|
||||||
* Includes a dashboard that shows basic statistics and has document upload.
|
- Includes a dashboard that shows basic statistics and has document upload.
|
||||||
* Filtering by tags, correspondents, types, and more.
|
- Filtering by tags, correspondents, types, and more.
|
||||||
* Customizable views can be saved and displayed on the dashboard.
|
- Customizable views can be saved and displayed on the dashboard.
|
||||||
* Full text search helps you find what you need.
|
- Full text search helps you find what you need.
|
||||||
* Auto completion suggests relevant words from your documents.
|
- Auto completion suggests relevant words from your documents.
|
||||||
* Results are sorted by relevance to your search query.
|
- Results are sorted by relevance to your search query.
|
||||||
* Highlighting shows you which parts of the document matched the query.
|
- Highlighting shows you which parts of the document matched the query.
|
||||||
* Searching for similar documents ("More like this")
|
- Searching for similar documents ("More like this")
|
||||||
* Email processing: Paperless adds documents from your email accounts.
|
- Email processing: Paperless adds documents from your email accounts.
|
||||||
* Configure multiple accounts and filters for each account.
|
- Configure multiple accounts and filters for each account.
|
||||||
* When adding documents from mail, paperless can move these mail to a new folder, mark them as read, flag them as important or delete them.
|
- When adding documents from mail, paperless can move these mail to a new folder, mark them as read, flag them as important or delete them.
|
||||||
* Machine learning powered document matching.
|
- Machine learning powered document matching.
|
||||||
* Paperless-ngx learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
|
- Paperless-ngx learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
|
||||||
* Optimized for multi core systems: Paperless-ngx consumes multiple documents in parallel.
|
- Optimized for multi core systems: Paperless-ngx consumes multiple documents in parallel.
|
||||||
* The integrated sanity checker makes sure that your document archive is in good health.
|
- The integrated sanity checker makes sure that your document archive is in good health.
|
||||||
* [More screenshots are available in the documentation](https://paperless-ngx.readthedocs.io/en/latest/screenshots.html).
|
- [More screenshots are available in the documentation](https://paperless-ngx.readthedocs.io/en/latest/screenshots.html).
|
||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ The easiest way to deploy paperless is docker-compose. The files in the [`/docke
|
|||||||
If you'd like to jump right in, you can configure a docker-compose environment with our install script:
|
If you'd like to jump right in, you can configure a docker-compose environment with our install script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/install-paperless-ngx.sh)"
|
bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can install the dependencies and setup apache and a database server yourself. The [documentation](https://paperless-ngx.readthedocs.io/en/latest/setup.html#installation) has a step by step guide on how to do it.
|
Alternatively, you can install the dependencies and setup apache and a database server yourself. The [documentation](https://paperless-ngx.readthedocs.io/en/latest/setup.html#installation) has a step by step guide on how to do it.
|
||||||
@@ -73,6 +73,7 @@ Alternatively, you can install the dependencies and setup apache and a database
|
|||||||
Migrating from Paperless-ng is easy, just drop in the new docker image! See the [documentation on migrating](https://paperless-ngx.readthedocs.io/en/latest/setup.html#migrating-from-paperless-ng) for more details.
|
Migrating from Paperless-ng is easy, just drop in the new docker image! See the [documentation on migrating](https://paperless-ngx.readthedocs.io/en/latest/setup.html#migrating-from-paperless-ng) for more details.
|
||||||
|
|
||||||
<!-- omit in toc -->
|
<!-- omit in toc -->
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
The documentation for Paperless-ngx is available on [ReadTheDocs](https://paperless-ngx.readthedocs.io/).
|
The documentation for Paperless-ngx is available on [ReadTheDocs](https://paperless-ngx.readthedocs.io/).
|
||||||
@@ -101,18 +102,18 @@ For bugs please [open an issue](https://github.com/paperless-ngx/paperless-ngx/i
|
|||||||
|
|
||||||
Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list:
|
Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list:
|
||||||
|
|
||||||
* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ngx.
|
- [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ngx.
|
||||||
* [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents.
|
- [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents.
|
||||||
* [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for Paperless.
|
- [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for Paperless.
|
||||||
|
|
||||||
These projects also exist, but their status and compatibility with paperless-ngx is unknown.
|
These projects also exist, but their status and compatibility with paperless-ngx is unknown.
|
||||||
|
|
||||||
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
|
- [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
|
||||||
|
|
||||||
This project also exists, but needs updates to be compatible with paperless-ngx.
|
This project also exists, but needs updates to be compatible with paperless-ngx.
|
||||||
|
|
||||||
* [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows.
|
- [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows.
|
||||||
Known issues on Mac: (Could not load reminders and documents)
|
Known issues on Mac: (Could not load reminders and documents)
|
||||||
|
|
||||||
# Important Note
|
# Important Note
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
# docker-compose file for running paperless from the Docker Hub.
|
# docker-compose file for running paperless from the docker container registry.
|
||||||
# This file contains everything paperless needs to run.
|
# This file contains everything paperless needs to run.
|
||||||
# Paperless supports amd64, arm and arm64 hardware.
|
# Paperless supports amd64, arm and arm64 hardware. The apache/tika image
|
||||||
|
# does not support arm or arm64, however.
|
||||||
#
|
#
|
||||||
# All compose files of paperless configure paperless in the following way:
|
# All compose files of paperless configure paperless in the following way:
|
||||||
#
|
#
|
||||||
@@ -79,8 +80,9 @@ services:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7
|
image: gotenberg/gotenberg:7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
command:
|
||||||
CHROMIUM_DISABLE_ROUTES: 1
|
- "gotenberg"
|
||||||
|
- "--chromium-disable-routes=true"
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: apache/tika
|
image: apache/tika
|
||||||
|
84
docker/compose/docker-compose.sqlite-tika.arm.yml
Normal file
84
docker/compose/docker-compose.sqlite-tika.arm.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# docker-compose file for running paperless from the docker container registry.
|
||||||
|
# This file contains everything paperless needs to run.
|
||||||
|
# Paperless supports amd64, arm and arm64 hardware.
|
||||||
|
#
|
||||||
|
# All compose files of paperless configure paperless in the following way:
|
||||||
|
#
|
||||||
|
# - Paperless is (re)started on system boot, if it was running before shutdown.
|
||||||
|
# - Docker volumes for storing data are managed by Docker.
|
||||||
|
# - Folders for importing and exporting files are created in the same directory
|
||||||
|
# as this file and mounted to the correct folders inside the container.
|
||||||
|
# - Paperless listens on port 8000.
|
||||||
|
#
|
||||||
|
# SQLite is used as the database. The SQLite file is stored in the data volume.
|
||||||
|
#
|
||||||
|
# iwishiwasaneagle/apache-tika-arm docker image is used to enable arm64 arch
|
||||||
|
# which apache/tika does not currently support.
|
||||||
|
#
|
||||||
|
# In addition to that, this docker-compose file adds the following optional
|
||||||
|
# configurations:
|
||||||
|
#
|
||||||
|
# - Apache Tika and Gotenberg servers are started with paperless and paperless
|
||||||
|
# is configured to use these services. These provide support for consuming
|
||||||
|
# Office documents (Word, Excel, Power Point and their LibreOffice counter-
|
||||||
|
# parts.
|
||||||
|
#
|
||||||
|
# To install and update paperless with this file, do the following:
|
||||||
|
#
|
||||||
|
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||||
|
# and '.env' into a folder.
|
||||||
|
# - Run 'docker-compose pull'.
|
||||||
|
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||||
|
# - Run 'docker-compose up -d'.
|
||||||
|
#
|
||||||
|
# For more extensive installation and update instructions, refer to the
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
broker:
|
||||||
|
image: redis:6.0
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
|
||||||
|
webserver:
|
||||||
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- broker
|
||||||
|
- gotenberg
|
||||||
|
- tika
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- data:/usr/src/paperless/data
|
||||||
|
- media:/usr/src/paperless/media
|
||||||
|
- ./export:/usr/src/paperless/export
|
||||||
|
- ./consume:/usr/src/paperless/consume
|
||||||
|
env_file: docker-compose.env
|
||||||
|
environment:
|
||||||
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|
||||||
|
gotenberg:
|
||||||
|
image: thecodingmachine/gotenberg
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DISABLE_GOOGLE_CHROME: 1
|
||||||
|
|
||||||
|
tika:
|
||||||
|
image: iwishiwasaneagle/apache-tika-arm@sha256:a78c25ffe57ecb1a194b2859d42a61af46e9e845191512b8f1a4bf90578ffdfd
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
media:
|
||||||
|
redisdata:
|
@@ -1,6 +1,7 @@
|
|||||||
# docker-compose file for running paperless from the Docker Hub.
|
# docker-compose file for running paperless from the docker container registry.
|
||||||
# This file contains everything paperless needs to run.
|
# This file contains everything paperless needs to run.
|
||||||
# Paperless supports amd64, arm and arm64 hardware.
|
# Paperless supports amd64, arm and arm64 hardware. The apache/tika image
|
||||||
|
# does not support arm or arm64, however.
|
||||||
#
|
#
|
||||||
# All compose files of paperless configure paperless in the following way:
|
# All compose files of paperless configure paperless in the following way:
|
||||||
#
|
#
|
||||||
@@ -68,8 +69,9 @@ services:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7
|
image: gotenberg/gotenberg:7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
command:
|
||||||
CHROMIUM_DISABLE_ROUTES: 1
|
- "gotenberg"
|
||||||
|
- "--chromium-disable-routes=true"
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: apache/tika
|
image: apache/tika
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ map_uidgid() {
|
|||||||
USERMAP_NEW_GID=${USERMAP_GID:-${USERMAP_ORIG_GID:-$USERMAP_NEW_UID}}
|
USERMAP_NEW_GID=${USERMAP_GID:-${USERMAP_ORIG_GID:-$USERMAP_NEW_UID}}
|
||||||
if [[ ${USERMAP_NEW_UID} != "${USERMAP_ORIG_UID}" || ${USERMAP_NEW_GID} != "${USERMAP_ORIG_GID}" ]]; then
|
if [[ ${USERMAP_NEW_UID} != "${USERMAP_ORIG_UID}" || ${USERMAP_NEW_GID} != "${USERMAP_ORIG_GID}" ]]; then
|
||||||
echo "Mapping UID and GID for paperless:paperless to $USERMAP_NEW_UID:$USERMAP_NEW_GID"
|
echo "Mapping UID and GID for paperless:paperless to $USERMAP_NEW_UID:$USERMAP_NEW_GID"
|
||||||
usermod -u "${USERMAP_NEW_UID}" paperless
|
usermod -o -u "${USERMAP_NEW_UID}" paperless
|
||||||
groupmod -o -g "${USERMAP_NEW_GID}" paperless
|
groupmod -o -g "${USERMAP_NEW_GID}" paperless
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -56,12 +56,12 @@ install_languages() {
|
|||||||
# continue
|
# continue
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
if dpkg -s $pkg &>/dev/null; then
|
if dpkg -s "$pkg" &>/dev/null; then
|
||||||
echo "Package $pkg already installed!"
|
echo "Package $pkg already installed!"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! apt-cache show $pkg &>/dev/null; then
|
if ! apt-cache show "$pkg" &>/dev/null; then
|
||||||
echo "Package $pkg not found! :("
|
echo "Package $pkg not found! :("
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -77,7 +77,7 @@ install_languages() {
|
|||||||
echo "Paperless-ngx docker container starting..."
|
echo "Paperless-ngx docker container starting..."
|
||||||
|
|
||||||
# Install additional languages if specified
|
# Install additional languages if specified
|
||||||
if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then
|
if [[ -n "$PAPERLESS_OCR_LANGUAGES" ]]; then
|
||||||
install_languages "$PAPERLESS_OCR_LANGUAGES"
|
install_languages "$PAPERLESS_OCR_LANGUAGES"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@@ -6,14 +6,11 @@ wait_for_postgres() {
|
|||||||
|
|
||||||
echo "Waiting for PostgreSQL to start..."
|
echo "Waiting for PostgreSQL to start..."
|
||||||
|
|
||||||
host="${PAPERLESS_DBHOST}"
|
host="${PAPERLESS_DBHOST:=localhost}"
|
||||||
port="${PAPERLESS_DBPORT}"
|
port="${PAPERLESS_DBPORT:=5342}"
|
||||||
|
|
||||||
if [[ -z $port ]]; then
|
|
||||||
port="5432"
|
|
||||||
fi
|
|
||||||
|
|
||||||
while ! </dev/tcp/$host/$port; do
|
while [ ! "$(pg_isready -h $host -p $port)" ]; do
|
||||||
|
|
||||||
if [ $attempt_num -eq $max_attempts ]; then
|
if [ $attempt_num -eq $max_attempts ]; then
|
||||||
echo "Unable to connect to database."
|
echo "Unable to connect to database."
|
||||||
@@ -23,7 +20,7 @@ wait_for_postgres() {
|
|||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
attempt_num=$(expr "$attempt_num" + 1)
|
attempt_num=$(("$attempt_num" + 1))
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker manage_superuser;
|
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker manage_superuser;
|
||||||
do
|
do
|
||||||
echo "installing $command..."
|
echo "installing $command..."
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -6,10 +6,10 @@ cd /usr/src/paperless/src/
|
|||||||
|
|
||||||
if [[ $(id -u) == 0 ]] ;
|
if [[ $(id -u) == 0 ]] ;
|
||||||
then
|
then
|
||||||
gosu paperless python3 manage.py management_command "$@"
|
gosu paperless python3 manage.py management_command "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]] ;
|
elif [[ $(id -un) == "paperless" ]] ;
|
||||||
then
|
then
|
||||||
python3 manage.py management_command "$@"
|
python3 manage.py management_command "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
fi
|
fi
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
FROM python:3.5.1
|
FROM python:3.5.1
|
||||||
MAINTAINER Pit Kleyersburg <pitkley@googlemail.com>
|
|
||||||
|
|
||||||
# Install Sphinx and Pygments
|
# Install Sphinx and Pygments
|
||||||
RUN pip install Sphinx Pygments
|
RUN pip install Sphinx Pygments
|
||||||
|
@@ -379,7 +379,7 @@ the naming scheme.
|
|||||||
|
|
||||||
The command takes no arguments and processes all your documents at once.
|
The command takes no arguments and processes all your documents at once.
|
||||||
|
|
||||||
Learn how to use :ref:`Management Utilities<Management utilities>`.
|
Learn how to use :ref:`Management Utilities<utilities-management-commands>`.
|
||||||
|
|
||||||
|
|
||||||
.. _utilities-sanity-checker:
|
.. _utilities-sanity-checker:
|
||||||
|
@@ -179,13 +179,14 @@ Assumed you have ``/home/foo/paperless-ngx/scripts/post-consumption-example.sh``
|
|||||||
You can pass that script into the consumer container via a host mount in your ``docker-compose.yml``.
|
You can pass that script into the consumer container via a host mount in your ``docker-compose.yml``.
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
...
|
|
||||||
consumer:
|
...
|
||||||
...
|
consumer:
|
||||||
volumes:
|
...
|
||||||
...
|
volumes:
|
||||||
- /home/paperless-ngx/scripts:/path/in/container/scripts/
|
...
|
||||||
...
|
- /home/paperless-ngx/scripts:/path/in/container/scripts/
|
||||||
|
...
|
||||||
|
|
||||||
Example (docker-compose.yml): ``- /home/foo/paperless-ngx/scripts:/usr/src/paperless/scripts``
|
Example (docker-compose.yml): ``- /home/foo/paperless-ngx/scripts:/usr/src/paperless/scripts``
|
||||||
|
|
||||||
|
@@ -60,7 +60,7 @@ The endpoints correctly serve the response header fields ``Content-Disposition``
|
|||||||
and ``Content-Type`` to indicate the filename for download and the type of content of
|
and ``Content-Type`` to indicate the filename for download and the type of content of
|
||||||
the document.
|
the document.
|
||||||
|
|
||||||
In order to download or preview the original document when an archied document is available,
|
In order to download or preview the original document when an archived document is available,
|
||||||
supply the query parameter ``original=true``.
|
supply the query parameter ``original=true``.
|
||||||
|
|
||||||
.. hint::
|
.. hint::
|
||||||
|
@@ -23,6 +23,7 @@ Version 1.6.0 merges several pending PRs from jonaswinkler's repo and includes n
|
|||||||
* `@shamoon`_ added 'any' / 'all' and 'not' filtering with tags (#10).
|
* `@shamoon`_ added 'any' / 'all' and 'not' filtering with tags (#10).
|
||||||
* `@shamoon`_ added warnings for unsaved changes, with smart edit buttons (#13).
|
* `@shamoon`_ added warnings for unsaved changes, with smart edit buttons (#13).
|
||||||
* `@benjaminfrank`_ enabled a non-root access to port 80 via systemd (#18).
|
* `@benjaminfrank`_ enabled a non-root access to port 80 via systemd (#18).
|
||||||
|
* `@tribut`_ added simple "delete to trash" functionality (#24). See ``PAPERLESS_TRASH_DIR``.
|
||||||
* `@amenk`_ fixed the search box overlay menu on mobile (#32).
|
* `@amenk`_ fixed the search box overlay menu on mobile (#32).
|
||||||
* `@dblitt`_ updated the login form to not auto-capitalize usernames (#36).
|
* `@dblitt`_ updated the login form to not auto-capitalize usernames (#36).
|
||||||
* `@evilsidekick293`_ made the worker timeout configurable (#37). See ``PAPERLESS_WORKER_TIMEOUT``.
|
* `@evilsidekick293`_ made the worker timeout configurable (#37). See ``PAPERLESS_WORKER_TIMEOUT``.
|
||||||
@@ -51,8 +52,11 @@ Another big thanks to the people who have contributed translations:
|
|||||||
* Lars Sørensen (Lrss) suggested 167 translations into Danish.
|
* Lars Sørensen (Lrss) suggested 167 translations into Danish.
|
||||||
* Philmo67 suggested 11 translations into French.
|
* Philmo67 suggested 11 translations into French.
|
||||||
|
|
||||||
|
Paperless-ng
|
||||||
|
############
|
||||||
|
|
||||||
paperless-ng 1.5.0
|
paperless-ng 1.5.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
Support for Python 3.6 was dropped.
|
Support for Python 3.6 was dropped.
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ Support for Python 3.6 was dropped.
|
|||||||
* `Daniel Albers`_ added support for making the files and folders ignored by the paperless consume folder scanner configurable. See ``PAPERLESS_CONSUMER_IGNORE_PATTERNS``.
|
* `Daniel Albers`_ added support for making the files and folders ignored by the paperless consume folder scanner configurable. See ``PAPERLESS_CONSUMER_IGNORE_PATTERNS``.
|
||||||
|
|
||||||
paperless-ng 1.4.5
|
paperless-ng 1.4.5
|
||||||
##################
|
==================
|
||||||
|
|
||||||
This is a maintenance release.
|
This is a maintenance release.
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ This is a maintenance release.
|
|||||||
with PDFminer even from non-PDF files.
|
with PDFminer even from non-PDF files.
|
||||||
|
|
||||||
paperless-ng 1.4.4
|
paperless-ng 1.4.4
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Drastically decreased the startup time of the docker container. The startup script adjusts file permissions of all data only if changes are required.
|
* Drastically decreased the startup time of the docker container. The startup script adjusts file permissions of all data only if changes are required.
|
||||||
* Paperless mail: Added ability to specify the character set for each server.
|
* Paperless mail: Added ability to specify the character set for each server.
|
||||||
@@ -85,7 +89,7 @@ paperless-ng 1.4.4
|
|||||||
* Updated translations.
|
* Updated translations.
|
||||||
|
|
||||||
paperless-ng 1.4.3
|
paperless-ng 1.4.3
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Additions and changes
|
* Additions and changes
|
||||||
|
|
||||||
@@ -102,12 +106,12 @@ paperless-ng 1.4.3
|
|||||||
* Fixed an issue with the document consumer crashing on certain documents due to issues with pdfminer.six. This library is used for PDF text extraction.
|
* Fixed an issue with the document consumer crashing on certain documents due to issues with pdfminer.six. This library is used for PDF text extraction.
|
||||||
|
|
||||||
paperless-ng 1.4.2
|
paperless-ng 1.4.2
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Fixed an issue with ``sudo`` that caused paperless to not start on many Raspberry Pi devices. Thank you `WhiteHatTux`_!
|
* Fixed an issue with ``sudo`` that caused paperless to not start on many Raspberry Pi devices. Thank you `WhiteHatTux`_!
|
||||||
|
|
||||||
paperless-ng 1.4.1
|
paperless-ng 1.4.1
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Added Polish locale.
|
* Added Polish locale.
|
||||||
|
|
||||||
@@ -123,7 +127,7 @@ paperless-ng 1.4.1
|
|||||||
``PAPERLESS_ADMIN_PASSWORD`` as environment variables to the docker container.
|
``PAPERLESS_ADMIN_PASSWORD`` as environment variables to the docker container.
|
||||||
|
|
||||||
paperless-ng 1.4.0
|
paperless-ng 1.4.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Docker images now use tesseract 4.1.1, which should fix a series of issues with OCR.
|
* Docker images now use tesseract 4.1.1, which should fix a series of issues with OCR.
|
||||||
|
|
||||||
@@ -156,7 +160,7 @@ paperless-ng 1.4.0
|
|||||||
(see :ref:`administration-index`).
|
(see :ref:`administration-index`).
|
||||||
|
|
||||||
paperless-ng 1.3.2
|
paperless-ng 1.3.2
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Added translation into Portuguese.
|
* Added translation into Portuguese.
|
||||||
|
|
||||||
@@ -171,7 +175,7 @@ paperless-ng 1.3.2
|
|||||||
* Fixed an issue with any/all/exact matching when characters used in regular expressions were used for the match.
|
* Fixed an issue with any/all/exact matching when characters used in regular expressions were used for the match.
|
||||||
|
|
||||||
paperless-ng 1.3.1
|
paperless-ng 1.3.1
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Added translation into Spanish and Russian.
|
* Added translation into Spanish and Russian.
|
||||||
|
|
||||||
@@ -196,7 +200,7 @@ paperless-ng 1.3.1
|
|||||||
* Fixed ``AUTO_LOGIN_USERNAME``: Unable to perform POST/PUT/DELETE requests and unable to receive WebSocket messages.
|
* Fixed ``AUTO_LOGIN_USERNAME``: Unable to perform POST/PUT/DELETE requests and unable to receive WebSocket messages.
|
||||||
|
|
||||||
paperless-ng 1.3.0
|
paperless-ng 1.3.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
This release contains new database migrations.
|
This release contains new database migrations.
|
||||||
|
|
||||||
@@ -222,7 +226,7 @@ This release contains new database migrations.
|
|||||||
|
|
||||||
|
|
||||||
paperless-ng 1.2.1
|
paperless-ng 1.2.1
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* `Rodrigo Avelino <https://github.com/rodavelino>`_ translated Paperless into Portuguese (Brazil)!
|
* `Rodrigo Avelino <https://github.com/rodavelino>`_ translated Paperless into Portuguese (Brazil)!
|
||||||
|
|
||||||
@@ -235,7 +239,7 @@ paperless-ng 1.2.1
|
|||||||
* Regression fix: Dates on the front end did not respect date locale settings in some cases.
|
* Regression fix: Dates on the front end did not respect date locale settings in some cases.
|
||||||
|
|
||||||
paperless-ng 1.2.0
|
paperless-ng 1.2.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Changes to the OCRmyPDF integration
|
* Changes to the OCRmyPDF integration
|
||||||
|
|
||||||
@@ -259,14 +263,14 @@ paperless-ng 1.2.0
|
|||||||
* Paperless no longer depends on ``libpoppler-cpp-dev``.
|
* Paperless no longer depends on ``libpoppler-cpp-dev``.
|
||||||
|
|
||||||
paperless-ng 1.1.4
|
paperless-ng 1.1.4
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Added English (GB) locale.
|
* Added English (GB) locale.
|
||||||
|
|
||||||
* Added ISO-8601 date display option.
|
* Added ISO-8601 date display option.
|
||||||
|
|
||||||
paperless-ng 1.1.3
|
paperless-ng 1.1.3
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Added a docker-specific configuration option to adjust the number of
|
* Added a docker-specific configuration option to adjust the number of
|
||||||
worker processes of the web server. See :ref:`configuration-docker`.
|
worker processes of the web server. See :ref:`configuration-docker`.
|
||||||
@@ -276,7 +280,7 @@ paperless-ng 1.1.3
|
|||||||
* Don't show inbox statistics if no inbox tag is defined.
|
* Don't show inbox statistics if no inbox tag is defined.
|
||||||
|
|
||||||
paperless-ng 1.1.2
|
paperless-ng 1.1.2
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Always show top left corner of thumbnails, even for extra wide documents.
|
* Always show top left corner of thumbnails, even for extra wide documents.
|
||||||
|
|
||||||
@@ -292,7 +296,7 @@ paperless-ng 1.1.2
|
|||||||
* Some memory usage optimizations.
|
* Some memory usage optimizations.
|
||||||
|
|
||||||
paperless-ng 1.1.1
|
paperless-ng 1.1.1
|
||||||
##################
|
==================
|
||||||
|
|
||||||
This release contains new database migrations.
|
This release contains new database migrations.
|
||||||
|
|
||||||
@@ -313,7 +317,7 @@ This release contains new database migrations.
|
|||||||
also ensure that they're always executed as the paperless user and you're less likely to run into permission issues. See :ref:`utilities-management-commands`.
|
also ensure that they're always executed as the paperless user and you're less likely to run into permission issues. See :ref:`utilities-management-commands`.
|
||||||
|
|
||||||
paperless-ng 1.1.0
|
paperless-ng 1.1.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Document processing status
|
* Document processing status
|
||||||
|
|
||||||
@@ -373,7 +377,7 @@ paperless-ng 1.1.0
|
|||||||
``PAPERLESS_LOGROTATE_MAX_BACKUPS``.
|
``PAPERLESS_LOGROTATE_MAX_BACKUPS``.
|
||||||
|
|
||||||
paperless-ng 1.0.0
|
paperless-ng 1.0.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
Nothing special about this release, but since there are relatively few bug reports coming in, I think that this is reasonably stable.
|
Nothing special about this release, but since there are relatively few bug reports coming in, I think that this is reasonably stable.
|
||||||
|
|
||||||
@@ -396,7 +400,7 @@ Nothing special about this release, but since there are relatively few bug repor
|
|||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.14
|
paperless-ng 0.9.14
|
||||||
###################
|
===================
|
||||||
|
|
||||||
Starting with this version, releases are getting built automatically. This release also comes with changes on how to install and
|
Starting with this version, releases are getting built automatically. This release also comes with changes on how to install and
|
||||||
update paperless.
|
update paperless.
|
||||||
@@ -444,13 +448,13 @@ update paperless.
|
|||||||
* An issue with the consumer crashing when invalid regular expression were used was fixed.
|
* An issue with the consumer crashing when invalid regular expression were used was fixed.
|
||||||
|
|
||||||
paperless-ng 0.9.13
|
paperless-ng 0.9.13
|
||||||
###################
|
===================
|
||||||
|
|
||||||
* Fixed an issue with Paperless not starting due to the new Tika integration when ``USERMAP_UID`` and ``USERMAP_GID`` was used
|
* Fixed an issue with Paperless not starting due to the new Tika integration when ``USERMAP_UID`` and ``USERMAP_GID`` was used
|
||||||
in the ``docker-compose.env`` file.
|
in the ``docker-compose.env`` file.
|
||||||
|
|
||||||
paperless-ng 0.9.12
|
paperless-ng 0.9.12
|
||||||
###################
|
===================
|
||||||
|
|
||||||
* Paperless localization
|
* Paperless localization
|
||||||
|
|
||||||
@@ -492,13 +496,13 @@ paperless-ng 0.9.12
|
|||||||
a document that did not yet exist in the database.
|
a document that did not yet exist in the database.
|
||||||
|
|
||||||
paperless-ng 0.9.11
|
paperless-ng 0.9.11
|
||||||
###################
|
===================
|
||||||
|
|
||||||
* Fixed an issue with the docker image not starting at all due to a configuration change of the web server.
|
* Fixed an issue with the docker image not starting at all due to a configuration change of the web server.
|
||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.10
|
paperless-ng 0.9.10
|
||||||
###################
|
===================
|
||||||
|
|
||||||
* Bulk editing
|
* Bulk editing
|
||||||
|
|
||||||
@@ -531,7 +535,7 @@ paperless-ng 0.9.10
|
|||||||
by :ref:`running the management command document_index with the argument reindex <administration-index>`.
|
by :ref:`running the management command document_index with the argument reindex <administration-index>`.
|
||||||
|
|
||||||
paperless-ng 0.9.9
|
paperless-ng 0.9.9
|
||||||
##################
|
==================
|
||||||
|
|
||||||
Christmas release!
|
Christmas release!
|
||||||
|
|
||||||
@@ -565,7 +569,7 @@ Christmas release!
|
|||||||
* Most of the guesswork features have been removed. Paperless no longer tries to extract correspondents and tags from file names.
|
* Most of the guesswork features have been removed. Paperless no longer tries to extract correspondents and tags from file names.
|
||||||
|
|
||||||
paperless-ng 0.9.8
|
paperless-ng 0.9.8
|
||||||
##################
|
==================
|
||||||
|
|
||||||
This release addresses two severe issues with the previous release.
|
This release addresses two severe issues with the previous release.
|
||||||
|
|
||||||
@@ -574,7 +578,7 @@ This release addresses two severe issues with the previous release.
|
|||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.7
|
paperless-ng 0.9.7
|
||||||
##################
|
==================
|
||||||
|
|
||||||
|
|
||||||
* Front end
|
* Front end
|
||||||
@@ -616,7 +620,7 @@ paperless-ng 0.9.7
|
|||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.6
|
paperless-ng 0.9.6
|
||||||
##################
|
==================
|
||||||
|
|
||||||
This release focusses primarily on many small issues with the UI.
|
This release focusses primarily on many small issues with the UI.
|
||||||
|
|
||||||
@@ -657,7 +661,7 @@ This release focusses primarily on many small issues with the UI.
|
|||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.5
|
paperless-ng 0.9.5
|
||||||
##################
|
==================
|
||||||
|
|
||||||
This release concludes the big changes I wanted to get rolled into paperless. The next releases before 1.0 will
|
This release concludes the big changes I wanted to get rolled into paperless. The next releases before 1.0 will
|
||||||
focus on fixing issues, primarily.
|
focus on fixing issues, primarily.
|
||||||
@@ -707,7 +711,7 @@ focus on fixing issues, primarily.
|
|||||||
* Assigning correspondents from mail sender names failed for very long names. Paperless no longer assigns correspondents in these cases.
|
* Assigning correspondents from mail sender names failed for very long names. Paperless no longer assigns correspondents in these cases.
|
||||||
|
|
||||||
paperless-ng 0.9.4
|
paperless-ng 0.9.4
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Searching:
|
* Searching:
|
||||||
|
|
||||||
@@ -735,7 +739,7 @@ paperless-ng 0.9.4
|
|||||||
how to setup the development environment.
|
how to setup the development environment.
|
||||||
|
|
||||||
paperless-ng 0.9.3
|
paperless-ng 0.9.3
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Setting ``PAPERLESS_AUTO_LOGIN_USERNAME`` replaces ``PAPERLESS_DISABLE_LOGIN``.
|
* Setting ``PAPERLESS_AUTO_LOGIN_USERNAME`` replaces ``PAPERLESS_DISABLE_LOGIN``.
|
||||||
You have to specify your username.
|
You have to specify your username.
|
||||||
@@ -753,7 +757,7 @@ paperless-ng 0.9.3
|
|||||||
* Added lots of tests for various parts of the application.
|
* Added lots of tests for various parts of the application.
|
||||||
|
|
||||||
paperless-ng 0.9.2
|
paperless-ng 0.9.2
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Major changes to the front end (colors, logo, shadows, layout of the cards,
|
* Major changes to the front end (colors, logo, shadows, layout of the cards,
|
||||||
better mobile support)
|
better mobile support)
|
||||||
@@ -783,7 +787,7 @@ paperless-ng 0.9.2
|
|||||||
up in the admin.
|
up in the admin.
|
||||||
|
|
||||||
paperless-ng 0.9.1
|
paperless-ng 0.9.1
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* Moved documentation of the settings to the actual documentation.
|
* Moved documentation of the settings to the actual documentation.
|
||||||
* Updated release script to force the user to choose between SQLite
|
* Updated release script to force the user to choose between SQLite
|
||||||
@@ -791,7 +795,7 @@ paperless-ng 0.9.1
|
|||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.0
|
paperless-ng 0.9.0
|
||||||
##################
|
==================
|
||||||
|
|
||||||
* **Deprecated:** GnuPG. :ref:`See this note on the state of GnuPG in paperless-ng. <utilities-encyption>`
|
* **Deprecated:** GnuPG. :ref:`See this note on the state of GnuPG in paperless-ng. <utilities-encyption>`
|
||||||
This features will most likely be removed in future versions.
|
This features will most likely be removed in future versions.
|
||||||
|
@@ -389,6 +389,15 @@ PAPERLESS_OCR_IMAGE_DPI=<num>
|
|||||||
Default is none, which will automatically calculate image DPI so that
|
Default is none, which will automatically calculate image DPI so that
|
||||||
the produced PDF documents are A4 sized.
|
the produced PDF documents are A4 sized.
|
||||||
|
|
||||||
|
PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>
|
||||||
|
Paperless will not OCR images that have more pixels than this limit.
|
||||||
|
This is intended to prevent decompression bombs from overloading paperless.
|
||||||
|
Increasing this limit is desired if you face a DecompressionBombError despite
|
||||||
|
the concerning file not being malicious; this could e.g. be caused by invalidly
|
||||||
|
recognized metadata.
|
||||||
|
If you have enough resources or if you are certain that your uploaded files
|
||||||
|
are not malicious you can increase this value to your needs.
|
||||||
|
The default value is 256000000, an image with more pixels than that would not be parsed.
|
||||||
|
|
||||||
PAPERLESS_OCR_USER_ARGS=<json>
|
PAPERLESS_OCR_USER_ARGS=<json>
|
||||||
OCRmyPDF offers many more options. Use this parameter to specify any
|
OCRmyPDF offers many more options. Use this parameter to specify any
|
||||||
@@ -462,8 +471,9 @@ requires are as follows:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7
|
image: gotenberg/gotenberg:7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
command:
|
||||||
CHROMIUM_DISABLE_ROUTES: 1
|
- "gotenberg"
|
||||||
|
- "--chromium-disable-routes=true"
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: apache/tika
|
image: apache/tika
|
||||||
@@ -473,6 +483,8 @@ Add the configuration variables to the environment of the webserver (alternative
|
|||||||
put the configuration in the ``docker-compose.env`` file) and add the additional
|
put the configuration in the ``docker-compose.env`` file) and add the additional
|
||||||
services below the webserver service. Watch out for indentation.
|
services below the webserver service. Watch out for indentation.
|
||||||
|
|
||||||
|
Make sure to use the correct format `PAPERLESS_TIKA_ENABLED = 1` so python_dotenv can parse the statement correctly.
|
||||||
|
|
||||||
Software tweaks
|
Software tweaks
|
||||||
###############
|
###############
|
||||||
|
|
||||||
|
@@ -1,145 +0,0 @@
|
|||||||
.. _contributing:
|
|
||||||
|
|
||||||
Contributing to Paperless
|
|
||||||
#########################
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
This section is not updated to paperless-ngx yet.
|
|
||||||
|
|
||||||
Maybe you've been using Paperless for a while and want to add a feature or two,
|
|
||||||
or maybe you've come across a bug that you have some ideas how to solve. The
|
|
||||||
beauty of Free software is that you can see what's wrong and help to get it
|
|
||||||
fixed for everyone!
|
|
||||||
|
|
||||||
|
|
||||||
How to Get Your Changes Rolled Into Paperless
|
|
||||||
=============================================
|
|
||||||
|
|
||||||
If you've found a bug, but don't know how to fix it, you can always post an
|
|
||||||
issue on `GitHub`_ in the hopes that someone will have the time to fix it for
|
|
||||||
you. If however you're the one with the time, pull requests are always
|
|
||||||
welcome, you just have to make sure that your code conforms to a few standards:
|
|
||||||
|
|
||||||
Pep8
|
|
||||||
----
|
|
||||||
|
|
||||||
It's the standard for all Python development, so it's `very well documented`_.
|
|
||||||
The short version is:
|
|
||||||
|
|
||||||
* Lines should wrap at 79 characters
|
|
||||||
* Use ``snake_case`` for variables, ``CamelCase`` for classes, and ``ALL_CAPS``
|
|
||||||
for constants.
|
|
||||||
* Space out your operators: ``stuff + 7`` instead of ``stuff+7``
|
|
||||||
* Two empty lines between classes, and functions, but 1 empty line between
|
|
||||||
class methods.
|
|
||||||
|
|
||||||
There's more to it than that, but if you follow those, you'll probably be
|
|
||||||
alright. When you submit your pull request, there's a pep8 checker that'll
|
|
||||||
look at your code to see if anything is off. If it finds anything, it'll
|
|
||||||
complain at you until you fix it.
|
|
||||||
|
|
||||||
|
|
||||||
Additional Style Guides
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
Where pep8 is ambiguous, I've tried to be a little more specific. These rules
|
|
||||||
aren't hard-and-fast, but if you can conform to them, I'll appreciate it and
|
|
||||||
spend less time trying to conform your PR before merging:
|
|
||||||
|
|
||||||
|
|
||||||
Function calls
|
|
||||||
..............
|
|
||||||
|
|
||||||
If you're calling a function and that necessitates more than one line of code,
|
|
||||||
please format it like this:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
my_function(
|
|
||||||
argument1,
|
|
||||||
kwarg1="x",
|
|
||||||
kwarg2="y"
|
|
||||||
another_really_long_kwarg="some big value"
|
|
||||||
a_kwarg_calling_another_long_function=another_function(
|
|
||||||
another_arg,
|
|
||||||
another_kwarg="kwarg!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
This is all in the interest of code uniformity rather than anything else. If
|
|
||||||
we stick to a style, everything is understandable in the same way.
|
|
||||||
|
|
||||||
|
|
||||||
Quoting Strings
|
|
||||||
...............
|
|
||||||
|
|
||||||
pep8 is a little too open-minded on this for my liking. Python strings should
|
|
||||||
be quoted with double quotes (``"``) except in cases where the resulting string
|
|
||||||
would require too much escaping of a double quote, in which case, a single
|
|
||||||
quoted, or triple-quoted string will do:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
my_string = "This is my string"
|
|
||||||
problematic_string = 'This is a "string" with "quotes" in it'
|
|
||||||
|
|
||||||
In HTML templates, please use double-quotes for tag attributes, and single
|
|
||||||
quotes for arguments passed to Django template tags:
|
|
||||||
|
|
||||||
.. code:: html
|
|
||||||
|
|
||||||
<div class="stuff">
|
|
||||||
<a href="{% url 'some-url-name' pk='w00t' %}">link this</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
This is to keep linters happy they look at an HTML file and see an attribute
|
|
||||||
closing the ``"`` before it should have been.
|
|
||||||
|
|
||||||
--
|
|
||||||
|
|
||||||
That's all there is in terms of guidelines, so I hope it's not too daunting.
|
|
||||||
|
|
||||||
|
|
||||||
Indentation & Spacing
|
|
||||||
.....................
|
|
||||||
|
|
||||||
When it comes to indentation:
|
|
||||||
|
|
||||||
* For Python, the rule is: follow pep8 and use 4 spaces.
|
|
||||||
* For Javascript, CSS, and HTML, please use 1 tab.
|
|
||||||
|
|
||||||
Additionally, Django templates making use of block elements like ``{% if %}``,
|
|
||||||
``{% for %}``, and ``{% block %}`` etc. should be indented:
|
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
.. code:: html
|
|
||||||
|
|
||||||
{% block stuff %}
|
|
||||||
<h1>This is the stuff</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
Bad:
|
|
||||||
|
|
||||||
.. code:: html
|
|
||||||
|
|
||||||
{% block stuff %}
|
|
||||||
<h1>This is the stuff</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
The Code of Conduct
|
|
||||||
===================
|
|
||||||
|
|
||||||
Paperless has a `code of conduct`_. It's a lot like the other ones you see out
|
|
||||||
there, with a few small changes, but basically it boils down to:
|
|
||||||
|
|
||||||
> Don't be an ass, or you might get banned.
|
|
||||||
|
|
||||||
I'm proud to say that the CoC has never had to be enforced because everyone has
|
|
||||||
been awesome, friendly, and professional.
|
|
||||||
|
|
||||||
.. _GitHub: https://github.com/the-paperless-project/paperless/issues
|
|
||||||
.. _very well documented: https://www.python.org/dev/peps/pep-0008/
|
|
||||||
.. _code of conduct: https://github.com/the-paperless-project/paperless/blob/master/CODE_OF_CONDUCT.md
|
|
@@ -1,13 +1,13 @@
|
|||||||
.. _extending:
|
.. _extending:
|
||||||
|
|
||||||
Paperless development
|
Paperless-ngx Development
|
||||||
#####################
|
#########################
|
||||||
|
|
||||||
This section describes the steps you need to take to start development on paperless-ngx.
|
This section describes the steps you need to take to start development on paperless-ngx.
|
||||||
|
|
||||||
Check out the source from github. The repository is organized in the following way:
|
Check out the source from github. The repository is organized in the following way:
|
||||||
|
|
||||||
* ``master`` always represents the latest release and will only see changes
|
* ``main`` always represents the latest release and will only see changes
|
||||||
when a new release is made.
|
when a new release is made.
|
||||||
* ``dev`` contains the code that will be in the next release.
|
* ``dev`` contains the code that will be in the next release.
|
||||||
* ``feature-X`` contain bigger changes that will be in some release, but not
|
* ``feature-X`` contain bigger changes that will be in some release, but not
|
||||||
@@ -23,6 +23,33 @@ Apart from that, the folder structure is as follows:
|
|||||||
* ``scripts/`` - Various scripts that help with different parts of development.
|
* ``scripts/`` - Various scripts that help with different parts of development.
|
||||||
* ``docker/`` - Files required to build the docker image.
|
* ``docker/`` - Files required to build the docker image.
|
||||||
|
|
||||||
|
Contributing to Paperless
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Maybe you've been using Paperless for a while and want to add a feature or two,
|
||||||
|
or maybe you've come across a bug that you have some ideas how to solve. The
|
||||||
|
beauty of open source software is that you can see what's wrong and help to get
|
||||||
|
it fixed for everyone!
|
||||||
|
|
||||||
|
Before contributing please review our `code of conduct`_ and other important
|
||||||
|
information in the `contributing guidelines`_.
|
||||||
|
|
||||||
|
.. _code-formatting-with-pre-commit-hooks:
|
||||||
|
|
||||||
|
Code formatting with pre-commit Hooks
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
To ensure a consistent style and formatting across the project source, the project
|
||||||
|
utilizes a Git `pre-commit` hook to perform some formatting and linting before a
|
||||||
|
commit is allowed. That way, everyone uses the same style and some common issues
|
||||||
|
can be caught early on. See below for installation instructions.
|
||||||
|
|
||||||
|
Once installed, hooks will run when you commit. If the formatting isn't quite right
|
||||||
|
or a linter catches something, the commit will be rejected. You'll need to look at the
|
||||||
|
output and fix the issue. Some hooks, such as the Python formatting tool `black`,
|
||||||
|
will format failing files, so all you need to do is `git add` those files again and
|
||||||
|
retry your commit.
|
||||||
|
|
||||||
Initial setup and first start
|
Initial setup and first start
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
@@ -37,13 +64,19 @@ To do the setup you need to perform the steps from the following chapters in a c
|
|||||||
|
|
||||||
$ npm install -g @angular/cli
|
$ npm install -g @angular/cli
|
||||||
|
|
||||||
4. Create ``consume`` and ``media`` folders in the cloned root folder.
|
4. Install pre-commit
|
||||||
|
|
||||||
|
.. code:: shell-session
|
||||||
|
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
5. Create ``consume`` and ``media`` folders in the cloned root folder.
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
mkdir -p consume media
|
mkdir -p consume media
|
||||||
|
|
||||||
5. You can now either ...
|
6. You can now either ...
|
||||||
|
|
||||||
* install redis or
|
* install redis or
|
||||||
* use the included scripts/start-services.sh to use docker to fire up a redis instance (and some other services such as tika, gotenberg and a postgresql server) or
|
* use the included scripts/start-services.sh to use docker to fire up a redis instance (and some other services such as tika, gotenberg and a postgresql server) or
|
||||||
@@ -53,41 +86,42 @@ To do the setup you need to perform the steps from the following chapters in a c
|
|||||||
|
|
||||||
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
||||||
|
|
||||||
6. Install the python dependencies by performing in the src/ directory.
|
7. Install the python dependencies by performing in the src/ directory.
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
|
|
||||||
* Make sure you're using python 3.9.x or lower. Otherwise you might get issues with building dependencies. You can use `pyenv <https://github.com/pyenv/pyenv>`_ to install a specific python version.
|
* Make sure you're using python 3.9.x or lower. Otherwise you might get issues with building dependencies. You can use `pyenv <https://github.com/pyenv/pyenv>`_ to install a specific python version.
|
||||||
|
|
||||||
7. Generate the static UI so you can perform a login to get session that is required for frontend development (this needs to be done one time only). From src-ui directory:
|
8. Generate the static UI so you can perform a login to get session that is required for frontend development (this needs to be done one time only). From src-ui directory:
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
npm install .
|
npm install .
|
||||||
./node_modules/.bin/ng build --configuration production
|
./node_modules/.bin/ng build --configuration production
|
||||||
|
|
||||||
8. Apply migrations and create a superuser for your dev instance:
|
9. Apply migrations and create a superuser for your dev instance:
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
python3 manage.py migrate
|
python3 manage.py migrate
|
||||||
python3 manage.py createsuperuser
|
python3 manage.py createsuperuser
|
||||||
|
|
||||||
9. Now spin up the dev backend. Depending on which part of paperless you're developing for, you need to have some or all of them running.
|
10. Now spin up the dev backend. Depending on which part of paperless you're developing for, you need to have some or all of them running.
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
python3 manage.py runserver & python3 manage.py document_consumer & python3 manage.py qcluster
|
python3 manage.py runserver & python3 manage.py document_consumer & python3 manage.py qcluster
|
||||||
|
|
||||||
10. Login with the superuser credentials provided in step 8 at ``http://localhost:8000`` to create a session that enables you to use the backend.
|
11. Login with the superuser credentials provided in step 8 at ``http://localhost:8000`` to create a session that enables you to use the backend.
|
||||||
|
|
||||||
Backend development environment is now ready, to start Frontend development go to ``/src-ui`` and run ``ng serve``. From there you can use ``http://localhost:4200`` for a preview.
|
Backend development environment is now ready, to start Frontend development go to ``/src-ui`` and run ``ng serve``. From there you can use ``http://localhost:4200`` for a preview.
|
||||||
|
|
||||||
Back end development
|
Back end development
|
||||||
====================
|
====================
|
||||||
|
|
||||||
The backend is a django application. I use PyCharm for development, but you can use whatever
|
The backend is a django application. PyCharm works well for development, but you can use whatever
|
||||||
you want.
|
you want.
|
||||||
|
|
||||||
Configure the IDE to use the src/ folder as the base source folder. Configure the following
|
Configure the IDE to use the src/ folder as the base source folder. Configure the following
|
||||||
@@ -108,8 +142,9 @@ Testing and code style:
|
|||||||
* Run ``pytest`` in the src/ directory to execute all tests. This also generates a HTML coverage
|
* Run ``pytest`` in the src/ directory to execute all tests. This also generates a HTML coverage
|
||||||
report. When runnings test, paperless.conf is loaded as well. However: the tests rely on the default
|
report. When runnings test, paperless.conf is loaded as well. However: the tests rely on the default
|
||||||
configuration. This is not ideal. But for now, make sure no settings except for DEBUG are overridden when testing.
|
configuration. This is not ideal. But for now, make sure no settings except for DEBUG are overridden when testing.
|
||||||
* Run ``black`` to format your code.
|
* Coding style is enforced by the Git pre-commit hooks. These will ensure your code is formatted and do some
|
||||||
* Run ``pycodestyle`` to test your code for issues with the configured code style settings.
|
linting when you do a `git commit`.
|
||||||
|
* You can also run ``black`` manually to format your code
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -121,9 +156,8 @@ Testing and code style:
|
|||||||
Front end development
|
Front end development
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
The front end is build using angular. I use the ``Code - OSS`` IDE for development.
|
The front end is built using Angular. In order to get started, you need ``npm``.
|
||||||
|
Install the Angular CLI interface with
|
||||||
In order to get started, you need ``npm``. Install the Angular CLI interface with
|
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
@@ -152,6 +186,31 @@ X-Frame-Options are in place so that the front end behaves exactly as in product
|
|||||||
relies on you being logged into the back end. Without a valid session, The front end will simply
|
relies on you being logged into the back end. Without a valid session, The front end will simply
|
||||||
not work.
|
not work.
|
||||||
|
|
||||||
|
Testing and code style:
|
||||||
|
|
||||||
|
* The frontend code (.ts, .html, .scss) use ``prettier`` for code formatting via the Git
|
||||||
|
``pre-commit`` hooks which run automatically on commit. See
|
||||||
|
:ref:`above <code-formatting-with-pre-commit-hooks>` for installation. You can also run this
|
||||||
|
via cli with a command such as
|
||||||
|
|
||||||
|
.. code:: shell-session
|
||||||
|
|
||||||
|
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||||
|
|
||||||
|
* Frontend testing uses jest and cypress. There is currently a need for significantly more
|
||||||
|
frontend tests. Unit tests and e2e tests, respectively, can be run non-interactively with:
|
||||||
|
|
||||||
|
.. code:: shell-session
|
||||||
|
|
||||||
|
$ ng test
|
||||||
|
$ npm run e2e:ci
|
||||||
|
|
||||||
|
Cypress also includes a UI which can be run from within the ``src-ui`` directory with
|
||||||
|
|
||||||
|
.. code:: shell-session
|
||||||
|
|
||||||
|
$ ./node_modules/.bin/cypress open
|
||||||
|
|
||||||
In order to build the front end and serve it as part of django, execute
|
In order to build the front end and serve it as part of django, execute
|
||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
@@ -361,3 +420,6 @@ that returns information about your parser:
|
|||||||
download. We could guess that from the file extensions, but some mime types have many extensions
|
download. We could guess that from the file extensions, but some mime types have many extensions
|
||||||
associated with them and the python methods responsible for guessing the extension do not always
|
associated with them and the python methods responsible for guessing the extension do not always
|
||||||
return the same value.
|
return the same value.
|
||||||
|
|
||||||
|
.. _code of conduct: https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md
|
||||||
|
.. _contributing guidelines: https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md
|
||||||
|
@@ -81,11 +81,10 @@ python requirements do not have precompiled packages for ARM / ARM64. Installati
|
|||||||
of these will require additional development libraries and compilation will take
|
of these will require additional development libraries and compilation will take
|
||||||
a long time.
|
a long time.
|
||||||
|
|
||||||
**Q:** *How do I run this on unRaid?*
|
**Q:** *How do I run this on Unraid?*
|
||||||
|
|
||||||
**A:** Head over to `<https://github.com/selfhosters/unRAID-CA-templates>`_,
|
**A:** Paperless-ngx is available as `community app <https://unraid.net/community/apps?q=paperless-ngx>`_
|
||||||
`Uli Fahrer <https://github.com/Tooa>`_ created a container template for that.
|
in Unraid. `Uli Fahrer <https://github.com/Tooa>`_ created a container template for that.
|
||||||
I don't exactly know how to use that though, since I don't use unRaid.
|
|
||||||
|
|
||||||
**Q:** *How do I run this on my toaster?*
|
**Q:** *How do I run this on my toaster?*
|
||||||
|
|
||||||
|
@@ -70,7 +70,6 @@ Contents
|
|||||||
faq
|
faq
|
||||||
troubleshooting
|
troubleshooting
|
||||||
extending
|
extending
|
||||||
contributing
|
|
||||||
scanners
|
scanners
|
||||||
screenshots
|
screenshots
|
||||||
changelog
|
changelog
|
||||||
|
@@ -112,3 +112,23 @@ On Android, you can use these applications in combination with one of the :ref:`
|
|||||||
|
|
||||||
.. _hannahswain: https://github.com/hannahswain
|
.. _hannahswain: https://github.com/hannahswain
|
||||||
.. _benjaminfrank: https://github.com/benjaminfrank
|
.. _benjaminfrank: https://github.com/benjaminfrank
|
||||||
|
|
||||||
|
API Scanning Setup
|
||||||
|
==================
|
||||||
|
|
||||||
|
This sections contains information on how to set up scanners to post directly to :ref:`Paperless API <api-file_uploads>`.
|
||||||
|
|
||||||
|
Doxie Q2
|
||||||
|
--------
|
||||||
|
|
||||||
|
This part assumes your Doxie is connected to WiFi and you know its IP.
|
||||||
|
|
||||||
|
1. Open your Doxie web UI by navigating to its IP address
|
||||||
|
2. Navigate to Options -> Webhook
|
||||||
|
3. Set the *URL* to ``https://[your-paperless-ngx-instance]/api/documents/post_document/``
|
||||||
|
4. Set the *File Parameter Name* to ``document``
|
||||||
|
5. Add the username and password to the respective fields (Consider creating a user just for your Doxie)
|
||||||
|
6. Click *Submit* at the bottom of the page
|
||||||
|
|
||||||
|
Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance!
|
||||||
|
|
||||||
|
@@ -42,4 +42,3 @@ Mobile support in the future? This kinda works, however some layouts are still
|
|||||||
too wide.
|
too wide.
|
||||||
|
|
||||||
.. image:: _static/screenshots/mobile.png
|
.. image:: _static/screenshots/mobile.png
|
||||||
|
|
||||||
|
@@ -110,7 +110,7 @@ performs all the steps described in :ref:`setup-docker_hub` automatically.
|
|||||||
|
|
||||||
.. code:: shell-session
|
.. code:: shell-session
|
||||||
|
|
||||||
$ bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/install-paperless-ngx.sh)"
|
$ bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||||
|
|
||||||
.. _setup-docker_hub:
|
.. _setup-docker_hub:
|
||||||
|
|
||||||
@@ -724,6 +724,8 @@ configuring some options in paperless can help improve performance immensely:
|
|||||||
times. Thumbnails will be about 20% larger.
|
times. Thumbnails will be about 20% larger.
|
||||||
* If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to
|
* If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to
|
||||||
1. This will save some memory.
|
1. This will save some memory.
|
||||||
|
* Use the arm compatible docker-compose if you're wanting to use Tika on something like
|
||||||
|
a raspberry pi. The official apache/tika image does not support the arm architecture.
|
||||||
|
|
||||||
For details, refer to :ref:`configuration`.
|
For details, refer to :ref:`configuration`.
|
||||||
|
|
||||||
|
@@ -25,6 +25,19 @@ Check for the following issues:
|
|||||||
* Go to the admin interface, and check if there are failed tasks. If so, the
|
* Go to the admin interface, and check if there are failed tasks. If so, the
|
||||||
tasks will contain an error message.
|
tasks will contain an error message.
|
||||||
|
|
||||||
|
Consumer warns ``OCR for XX failed``
|
||||||
|
####################################
|
||||||
|
|
||||||
|
If you find the OCR accuracy to be too low, and/or the document consumer warns
|
||||||
|
that ``OCR for XX failed, but we're going to stick with what we've got since
|
||||||
|
FORGIVING_OCR is enabled``, then you might need to install the
|
||||||
|
`Tesseract language files <http://packages.ubuntu.com/search?keywords=tesseract-ocr>`_
|
||||||
|
marching your document's languages.
|
||||||
|
|
||||||
|
As an example, if you are running Paperless-ngx from any Ubuntu or Debian
|
||||||
|
box, and your documents are written in Spanish you may need to run::
|
||||||
|
|
||||||
|
apt-get install -y tesseract-ocr-spa
|
||||||
|
|
||||||
Consumer fails to pickup any new files
|
Consumer fails to pickup any new files
|
||||||
######################################
|
######################################
|
||||||
@@ -106,7 +119,7 @@ You may experience these errors when using the optional TIKA integration:
|
|||||||
Gotenberg is a server that converts Office documents into PDF documents and has a default timeout of 30 seconds.
|
Gotenberg is a server that converts Office documents into PDF documents and has a default timeout of 30 seconds.
|
||||||
When conversion takes longer, Gotenberg raises this error.
|
When conversion takes longer, Gotenberg raises this error.
|
||||||
|
|
||||||
You can increase the timeout by configuring an environment variable for Gotenberg (see also `here <https://gotenberg.dev/docs/modules/api#properties>`__).
|
You can increase the timeout by configuring a command flag for Gotenberg (see also `here <https://gotenberg.dev/docs/modules/api#properties>`__).
|
||||||
If using docker-compose, this is achieved by the following configuration change in the ``docker-compose.yml`` file:
|
If using docker-compose, this is achieved by the following configuration change in the ``docker-compose.yml`` file:
|
||||||
|
|
||||||
.. code:: yaml
|
.. code:: yaml
|
||||||
@@ -114,9 +127,10 @@ If using docker-compose, this is achieved by the following configuration change
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7
|
image: gotenberg/gotenberg:7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
command:
|
||||||
CHROMIUM_DISABLE_ROUTES: 1
|
- "gotenberg"
|
||||||
API_PROCESS_TIMEOUT: 60
|
- "--chromium-disable-routes=true"
|
||||||
|
- "--api-timeout=60"
|
||||||
|
|
||||||
Permission denied errors in the consumption directory
|
Permission denied errors in the consumption directory
|
||||||
#####################################################
|
#####################################################
|
||||||
|
@@ -3,16 +3,16 @@
|
|||||||
ask() {
|
ask() {
|
||||||
while true ; do
|
while true ; do
|
||||||
if [[ -z $3 ]] ; then
|
if [[ -z $3 ]] ; then
|
||||||
read -p "$1 [$2]: " result
|
read -r -p "$1 [$2]: " result
|
||||||
else
|
else
|
||||||
read -p "$1 ($3) [$2]: " result
|
read -r -p "$1 ($3) [$2]: " result
|
||||||
fi
|
fi
|
||||||
if [[ -z $result ]]; then
|
if [[ -z $result ]]; then
|
||||||
ask_result=$2
|
ask_result=$2
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
array=$3
|
array=$3
|
||||||
if [[ -z $3 || " ${array[@]} " =~ " ${result} " ]]; then
|
if [[ -z $3 || " ${array[*]} " =~ ${result} ]]; then
|
||||||
ask_result=$result
|
ask_result=$result
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
@@ -24,7 +24,7 @@ ask() {
|
|||||||
ask_docker_folder() {
|
ask_docker_folder() {
|
||||||
while true ; do
|
while true ; do
|
||||||
|
|
||||||
read -p "$1 [$2]: " result
|
read -r -p "$1 [$2]: " result
|
||||||
|
|
||||||
if [[ -z $result ]]; then
|
if [[ -z $result ]]; then
|
||||||
ask_result=$2
|
ask_result=$2
|
||||||
@@ -64,8 +64,7 @@ fi
|
|||||||
|
|
||||||
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
|
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
|
||||||
# If this fails, the user probably does not have permissions for Docker.
|
# If this fails, the user probably does not have permissions for Docker.
|
||||||
docker stats --no-stream 2>/dev/null 1>&2
|
if [ ! "$(docker stats --no-stream 2>/dev/null 1>&2)" ] ; then
|
||||||
if [ $? -ne 0 ] ; then
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "WARN: It look like the current user does not have Docker permissions."
|
echo "WARN: It look like the current user does not have Docker permissions."
|
||||||
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user."
|
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user."
|
||||||
@@ -162,7 +161,7 @@ ask "Target folder" "$(pwd)/paperless-ngx"
|
|||||||
TARGET_FOLDER=$ask_result
|
TARGET_FOLDER=$ask_result
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "The consume folder is where paperles will search for new documents."
|
echo "The consume folder is where paperless will search for new documents."
|
||||||
echo "Point this to a folder where your scanner is able to put your scanned"
|
echo "Point this to a folder where your scanner is able to put your scanned"
|
||||||
echo "documents."
|
echo "documents."
|
||||||
echo ""
|
echo ""
|
||||||
@@ -228,7 +227,7 @@ ask "Paperless username" "$(whoami)"
|
|||||||
USERNAME=$ask_result
|
USERNAME=$ask_result
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -sp "Paperless password: " PASSWORD
|
read -r -sp "Paperless password: " PASSWORD
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -z $PASSWORD ]] ; then
|
if [[ -z $PASSWORD ]] ; then
|
||||||
@@ -236,7 +235,7 @@ while true; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -sp "Paperless password (again): " PASSWORD_REPEAT
|
read -r -sp "Paperless password (again): " PASSWORD_REPEAT
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ ! "$PASSWORD" == "$PASSWORD_REPEAT" ]] ; then
|
if [[ ! "$PASSWORD" == "$PASSWORD_REPEAT" ]] ; then
|
||||||
@@ -285,7 +284,7 @@ echo "Paperless username: $USERNAME"
|
|||||||
echo "Paperless email: $EMAIL"
|
echo "Paperless email: $EMAIL"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Press any key to install."
|
read -r -p "Press any key to install."
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installing paperless..."
|
echo "Installing paperless..."
|
||||||
@@ -301,10 +300,10 @@ if [[ $TIKA_ENABLED == "yes" ]] ; then
|
|||||||
DOCKER_COMPOSE_VERSION="$DOCKER_COMPOSE_VERSION-tika"
|
DOCKER_COMPOSE_VERSION="$DOCKER_COMPOSE_VERSION-tika"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
||||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/docker/compose/.env" -O .env
|
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
|
||||||
|
|
||||||
SECRET_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1)
|
SECRET_KEY=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1)
|
||||||
|
|
||||||
DEFAULT_LANGUAGES="deu eng fra ita spa"
|
DEFAULT_LANGUAGES="deu eng fra ita spa"
|
||||||
|
|
||||||
@@ -318,7 +317,7 @@ DEFAULT_LANGUAGES="deu eng fra ita spa"
|
|||||||
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
|
||||||
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
|
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
|
||||||
echo "PAPERLESS_SECRET_KEY=$SECRET_KEY"
|
echo "PAPERLESS_SECRET_KEY=$SECRET_KEY"
|
||||||
if [[ ! " ${DEFAULT_LANGUAGES[@]} " =~ " ${OCR_LANGUAGE} " ]] ; then
|
if [[ ! " ${DEFAULT_LANGUAGES[*]} " =~ ${OCR_LANGUAGE} ]] ; then
|
||||||
echo "PAPERLESS_OCR_LANGUAGES=$OCR_LANGUAGE"
|
echo "PAPERLESS_OCR_LANGUAGES=$OCR_LANGUAGE"
|
||||||
fi
|
fi
|
||||||
} > docker-compose.env
|
} > docker-compose.env
|
||||||
@@ -329,16 +328,29 @@ sed -i "s#- \./consume:/usr/src/paperless/consume#- $CONSUME_FOLDER:/usr/src/pap
|
|||||||
|
|
||||||
if [[ -n $MEDIA_FOLDER ]] ; then
|
if [[ -n $MEDIA_FOLDER ]] ; then
|
||||||
sed -i "s#- media:/usr/src/paperless/media#- $MEDIA_FOLDER:/usr/src/paperless/media#g" docker-compose.yml
|
sed -i "s#- media:/usr/src/paperless/media#- $MEDIA_FOLDER:/usr/src/paperless/media#g" docker-compose.yml
|
||||||
|
sed -i "/^\s*media:/d" docker-compose.yml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $DATA_FOLDER ]] ; then
|
if [[ -n $DATA_FOLDER ]] ; then
|
||||||
sed -i "s#- data:/usr/src/paperless/data#- $DATA_FOLDER:/usr/src/paperless/data#g" docker-compose.yml
|
sed -i "s#- data:/usr/src/paperless/data#- $DATA_FOLDER:/usr/src/paperless/data#g" docker-compose.yml
|
||||||
|
sed -i "/^\s*data:/d" docker-compose.yml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $POSTGRES_FOLDER ]] ; then
|
if [[ -n $POSTGRES_FOLDER ]] ; then
|
||||||
sed -i "s#- pgdata:/var/lib/postgresql/data#- $POSTGRES_FOLDER:/var/lib/postgresql/data#g" docker-compose.yml
|
sed -i "s#- pgdata:/var/lib/postgresql/data#- $POSTGRES_FOLDER:/var/lib/postgresql/data#g" docker-compose.yml
|
||||||
|
sed -i "/^\s*pgdata:/d" docker-compose.yml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# remove trailing blank lines from end of file
|
||||||
|
sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' docker-compose.yml
|
||||||
|
# if last line in file contains "volumes:", remove that line since no more named volumes are left
|
||||||
|
l1=$(grep -n '^volumes:' docker-compose.yml | cut -d : -f 1) # get line number containing volume: at begin of line
|
||||||
|
l2=$(wc -l < docker-compose.yml) # get total number of lines
|
||||||
|
if [ "$l1" -eq "$l2" ] ; then
|
||||||
|
sed -i "/^volumes:/d" docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
docker-compose pull
|
docker-compose pull
|
||||||
|
|
||||||
docker-compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
|
docker-compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
|
||||||
|
@@ -8,17 +8,18 @@
|
|||||||
-i https://pypi.python.org/simple
|
-i https://pypi.python.org/simple
|
||||||
--extra-index-url https://www.piwheels.org/simple
|
--extra-index-url https://www.piwheels.org/simple
|
||||||
aioredis==1.3.1
|
aioredis==1.3.1
|
||||||
|
anyio==3.5.0; python_full_version >= '3.6.2'
|
||||||
arrow==1.2.2; python_version >= '3.6'
|
arrow==1.2.2; python_version >= '3.6'
|
||||||
asgiref==3.5.0; python_version >= '3.7'
|
asgiref==3.5.0; python_version >= '3.7'
|
||||||
async-timeout==4.0.2; python_version >= '3.6'
|
async-timeout==4.0.2; python_version >= '3.6'
|
||||||
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
autobahn==22.2.2; python_version >= '3.7'
|
autobahn==22.2.2; python_version >= '3.7'
|
||||||
automat==20.2.0
|
automat==20.2.0
|
||||||
backports.zoneinfo==0.2.1
|
backports.zoneinfo==0.2.1; python_version < '3.9'
|
||||||
blessed==1.19.1; python_version >= '2.7'
|
blessed==1.19.1; python_version >= '2.7'
|
||||||
certifi==2021.10.8
|
certifi==2021.10.8
|
||||||
cffi==1.15.0
|
cffi==1.15.0
|
||||||
channels-redis==3.3.1
|
channels-redis==3.4.0
|
||||||
channels==3.0.4
|
channels==3.0.4
|
||||||
chardet==4.0.0; python_version >= '3.1'
|
chardet==4.0.0; python_version >= '3.1'
|
||||||
charset-normalizer==2.0.12; python_version >= '3'
|
charset-normalizer==2.0.12; python_version >= '3'
|
||||||
@@ -26,28 +27,28 @@ click==8.0.4; python_version >= '3.6'
|
|||||||
coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
concurrent-log-handler==0.9.20
|
concurrent-log-handler==0.9.20
|
||||||
constantly==15.1.0
|
constantly==15.1.0
|
||||||
cryptography==36.0.1
|
cryptography==36.0.2; python_version >= '3.6'
|
||||||
daphne==3.0.2; python_version >= '3.6'
|
daphne==3.0.2; python_version >= '3.6'
|
||||||
dateparser==1.1.0
|
dateparser==1.1.1
|
||||||
django-cors-headers==3.11.0
|
django-cors-headers==3.11.0
|
||||||
django-extensions==3.1.5
|
django-extensions==3.1.5
|
||||||
django-filter==21.1
|
django-filter==21.1
|
||||||
django-picklefield==3.0.1; python_version >= '3'
|
django-picklefield==3.0.1; python_version >= '3'
|
||||||
django-q==1.3.9
|
django-q==1.3.9
|
||||||
django==3.2.12
|
django==4.0.3
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
filelock==3.6.0
|
filelock==3.6.0
|
||||||
fuzzywuzzy[speedup]==0.18.0
|
fuzzywuzzy[speedup]==0.18.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
h11==0.13.0; python_version >= '3.6'
|
h11==0.13.0; python_version >= '3.6'
|
||||||
hiredis==2.0.0; python_version >= '3.6'
|
hiredis==2.0.0; python_version >= '3.6'
|
||||||
httptools==0.3.0
|
httptools==0.4.0
|
||||||
humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
hyperlink==21.0.0
|
hyperlink==21.0.0
|
||||||
idna==3.3; python_version >= '3.5'
|
idna==3.3; python_version >= '3.5'
|
||||||
imap-tools==0.51.1
|
imap-tools==0.53.0
|
||||||
img2pdf==0.4.3
|
img2pdf==0.4.3
|
||||||
importlib-resources==5.4.0; python_version < '3.9'
|
importlib-resources==5.6.0; python_version < '3.9'
|
||||||
incremental==21.3.0
|
incremental==21.3.0
|
||||||
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
inotifyrecursive==0.3.5
|
inotifyrecursive==0.3.5
|
||||||
@@ -55,55 +56,56 @@ joblib==1.1.0; python_version >= '3.6'
|
|||||||
langdetect==1.0.9
|
langdetect==1.0.9
|
||||||
lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
msgpack==1.0.3
|
msgpack==1.0.3
|
||||||
numpy==1.22.2
|
numpy==1.22.3; python_version >= '3.8'
|
||||||
ocrmypdf==13.4.0
|
ocrmypdf==13.4.1
|
||||||
packaging==21.3; python_version >= '3.6'
|
packaging==21.3; python_version >= '3.6'
|
||||||
pathvalidate==2.5.0
|
pathvalidate==2.5.0
|
||||||
pdfminer.six==20211012
|
pdfminer.six==20211012
|
||||||
pikepdf==5.0.1
|
pikepdf==5.1.0
|
||||||
pillow==9.0.1
|
pillow==9.0.1
|
||||||
pluggy==1.0.0; python_version >= '3.6'
|
pluggy==1.0.0; python_version >= '3.6'
|
||||||
portalocker==2.4.0; python_version >= '3'
|
portalocker==2.4.0; python_version >= '3'
|
||||||
psycopg2-binary==2.9.3
|
psycopg2==2.9.3
|
||||||
pyasn1-modules==0.2.8
|
pyasn1-modules==0.2.8
|
||||||
pyasn1==0.4.8
|
pyasn1==0.4.8
|
||||||
pycparser==2.21; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
pycparser==2.21
|
||||||
pyopenssl==22.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
pyopenssl==22.0.0
|
||||||
pyparsing==3.0.7; python_version >= '3.6'
|
pyparsing==3.0.7; python_version >= '3.6'
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
python-dotenv==0.19.2
|
python-dotenv==0.20.0
|
||||||
python-gnupg==0.4.8
|
python-gnupg==0.4.8
|
||||||
python-levenshtein==0.12.2
|
python-levenshtein==0.12.2
|
||||||
python-magic==0.4.25
|
python-magic==0.4.25
|
||||||
pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||||
pytz==2021.3
|
pytz==2022.1
|
||||||
pyyaml==6.0
|
pyyaml==6.0
|
||||||
redis==3.5.3
|
redis==3.5.3
|
||||||
regex==2022.1.18
|
regex==2022.3.2; python_version >= '3.6'
|
||||||
reportlab==3.6.7; python_version >= '3.6' and python_version < '4'
|
reportlab==3.6.9; python_version >= '3.7' and python_version < '4'
|
||||||
requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||||
scikit-learn==0.24.0
|
scikit-learn==1.0.2
|
||||||
scipy==1.8.0; python_version < '3.11' and python_version >= '3.8'
|
scipy==1.8.0; python_version < '3.11' and python_version >= '3.8'
|
||||||
service-identity==21.1.0
|
service-identity==21.1.0
|
||||||
setuptools==60.9.3; python_version >= '3.7'
|
setuptools==61.1.1; python_version >= '3.7'
|
||||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
|
sniffio==1.2.0; python_version >= '3.5'
|
||||||
sqlparse==0.4.2; python_version >= '3.5'
|
sqlparse==0.4.2; python_version >= '3.5'
|
||||||
threadpoolctl==3.1.0; python_version >= '3.6'
|
threadpoolctl==3.1.0; python_version >= '3.6'
|
||||||
tika==1.24
|
tika==1.24
|
||||||
tqdm==4.62.3
|
tqdm==4.63.1
|
||||||
twisted[tls]==22.1.0; python_full_version >= '3.6.7'
|
twisted[tls]==22.2.0; python_full_version >= '3.6.7'
|
||||||
txaio==22.2.1; python_version >= '3.6'
|
txaio==22.2.1; python_version >= '3.6'
|
||||||
typing-extensions==4.1.1; python_version >= '3.6'
|
typing-extensions==4.1.1; python_version >= '3.6'
|
||||||
tzdata==2021.5; python_version >= '3.6'
|
tzdata==2022.1; python_version >= '3.6'
|
||||||
tzlocal==4.1; python_version >= '3.6'
|
tzlocal==4.1; python_version >= '3.6'
|
||||||
urllib3==1.26.8; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||||
uvicorn[standard]==0.17.5
|
uvicorn[standard]==0.17.6
|
||||||
uvloop==0.16.0
|
uvloop==0.16.0
|
||||||
watchdog==2.1.6
|
watchdog==2.1.7
|
||||||
watchgod==0.7
|
watchgod==0.8.1
|
||||||
wcwidth==0.2.5
|
wcwidth==0.2.5
|
||||||
websockets==10.2
|
websockets==10.2
|
||||||
whitenoise==6.0.0
|
whitenoise==6.0.0
|
||||||
whoosh==2.7.4
|
whoosh==2.7.4
|
||||||
zipp==3.7.0; python_version < '3.10'
|
zipp==3.7.0; python_version < '3.9'
|
||||||
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||||
docker run -d -p 6379:6379 redis:latest
|
docker run -d -p 6379:6379 redis:latest
|
||||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7
|
docker run -p 3000:3000 -d gotenberg/gotenberg:7
|
||||||
|
4
src-ui/.gitignore
vendored
4
src-ui/.gitignore
vendored
@@ -45,3 +45,7 @@ testem.log
|
|||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress/videos/**/*
|
||||||
|
cypress/screenshots/**/*
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
"i18n": {
|
"i18n": {
|
||||||
"sourceLocale": "en-US",
|
"sourceLocale": "en-US",
|
||||||
"locales": {
|
"locales": {
|
||||||
|
"be-BY": "src/locale/messages.be_BY.xlf",
|
||||||
"cs-CZ": "src/locale/messages.cs_CZ.xlf",
|
"cs-CZ": "src/locale/messages.cs_CZ.xlf",
|
||||||
"da-DK": "src/locale/messages.da_DK.xlf",
|
"da-DK": "src/locale/messages.da_DK.xlf",
|
||||||
"de-DE": "src/locale/messages.de_DE.xlf",
|
"de-DE": "src/locale/messages.de_DE.xlf",
|
||||||
@@ -30,8 +31,12 @@
|
|||||||
"pt-PT": "src/locale/messages.pt_PT.xlf",
|
"pt-PT": "src/locale/messages.pt_PT.xlf",
|
||||||
"ro-RO": "src/locale/messages.ro_RO.xlf",
|
"ro-RO": "src/locale/messages.ro_RO.xlf",
|
||||||
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
||||||
"sv-SE": "src/locale/messages.sv_SE.xlf"
|
"sl-SI": "src/locale/messages.sl_SI.xlf",
|
||||||
}
|
"sr-CS": "src/locale/messages.sr_CS.xlf",
|
||||||
|
"sv-SE": "src/locale/messages.sv_SE.xlf",
|
||||||
|
"tr-TR": "src/locale/messages.tr_TR.xlf",
|
||||||
|
"zh-CN": "src/locale/messages.zh_CN.xlf"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
@@ -121,12 +126,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-builders/jest:run",
|
||||||
"options": {
|
"options": {
|
||||||
"main": "src/test.ts",
|
|
||||||
"polyfills": "src/polyfills.ts",
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"karmaConfig": "karma.conf.js",
|
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/apple-touch-icon.png",
|
"src/apple-touch-icon.png",
|
||||||
@@ -140,9 +142,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"builder": "@angular-devkit/build-angular:protractor",
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"devServerTarget": "paperless-ui:serve",
|
||||||
|
"watch": true,
|
||||||
|
"headless": false
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "paperless-ui:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cypress-run": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
"protractorConfig": "e2e/protractor.conf.js",
|
|
||||||
"devServerTarget": "paperless-ui:serve"
|
"devServerTarget": "paperless-ui:serve"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@@ -150,6 +164,13 @@
|
|||||||
"devServerTarget": "paperless-ui:serve:production"
|
"devServerTarget": "paperless-ui:serve:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cypress-open": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"watch": true,
|
||||||
|
"headless": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src-ui/cypress.json
Normal file
9
src-ui/cypress.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"integrationFolder": "cypress/integration",
|
||||||
|
"supportFile": "cypress/support/index.ts",
|
||||||
|
"videosFolder": "cypress/videos",
|
||||||
|
"screenshotsFolder": "cypress/screenshots",
|
||||||
|
"pluginsFile": "cypress/plugins/index.ts",
|
||||||
|
"fixturesFolder": "cypress/fixtures",
|
||||||
|
"baseUrl": "http://localhost:4200"
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
{"count":27,"next":"http://localhost:8000/api/correspondents/?page=2","previous":null,"results":[{"id":9,"slug":"abc-test-correspondent","name":"ABC Test Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":13,"slug":"corresp-10","name":"Corresp 10","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":14,"slug":"corresp-11","name":"Corresp 11","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":15,"slug":"corresp-12","name":"Corresp 12","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":16,"slug":"corresp-13","name":"Corresp 13","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":18,"slug":"corresp-15","name":"Corresp 15","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":19,"slug":"corresp-16","name":"Corresp 16","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":20,"slug":"corresp-17","name":"Corresp 17","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":21,"slug":"corresp-18","name":"Corresp 18","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":22,"slug":"corresp-19","name":"Corresp 19","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":23,"slug":"corresp-20","name":"Corresp 20","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":24,"slug":"corresp-21","name":"Corresp 21","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":25,"slug":"corresp-22","name":"Corresp 22","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":26,"slug":"corresp-23","name":"Corresp 23","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":5,"slug":"corresp-3","name":"Corresp 3","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":6,"slug":"corresp-4","name":"Corresp 4","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":7,"slug":"corresp-5","name":"Corresp 5","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":8,"slug":"corresp-6","name":"Corresp 6","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":10,"slug":"corresp-7","name":"Corresp 7","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":11,"slug":"corresp-8","name":"Corresp 8","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":12,"slug":"corresp-9","name":"Corresp 9","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":17,"slug":"correspondent-14","name":"Correspondent 14","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":2,"slug":"correspondent-2","name":"Correspondent 2","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":7,"last_correspondence":"2021-01-20T23:37:58.204614Z"},{"id":27,"slug":"michael-shamoon","name":"Michael Shamoon","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2022-03-16T03:48:50.089624Z"},{"id":4,"slug":"newest-correspondent","name":"Newest Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2021-02-07T08:00:00Z"}]}
|
1
src-ui/cypress/fixtures/document_types/doctypes.json
Normal file
1
src-ui/cypress/fixtures/document_types/doctypes.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"count":1,"next":null,"previous":null,"results":[{"id":1,"slug":"test","name":"Test Doc Type","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0}]}
|
1
src-ui/cypress/fixtures/documents/1/metadata.json
Normal file
1
src-ui/cypress/fixtures/documents/1/metadata.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"original_checksum":"e959bc7d593245d92685213264e962ba","original_size":963754,"original_mime_type":"application/pdf","media_filename":"2022/lorem-ipsum.pdf","has_archive_version":true,"original_metadata":[],"archive_checksum":"5a1f46a9150bcade978c764b039ce4d0","archive_media_filename":"2022/lorem-ipsum.pdf","archive_size":351160,"archive_metadata":[{"namespace":"http://ns.adobe.com/pdf/1.3/","prefix":"pdf","key":"Producer","value":"pikepdf5.0.1"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"ModifyDate","value":"2022-03-22T04:53:18+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreateDate","value":"2022-03-22T18:05:43+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreatorTool","value":"ocrmypdf13.4.0/TesseractOCR-PDF4.1.1"},{"namespace":"http://ns.adobe.com/xap/1.0/mm/","prefix":"xmpMM","key":"DocumentID","value":"uuid:df27edcf-e34a-11f7-0000-8fa6067a3c04"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"format","value":"application/pdf"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"title","value":"ScannedDocument"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"part","value":"2"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"conformance","value":"B"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"creator","value":"None"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"MetadataDate","value":"2022-03-22T21:53:18.882551-07:00"}]}
|
1
src-ui/cypress/fixtures/documents/1/suggestions.json
Normal file
1
src-ui/cypress/fixtures/documents/1/suggestions.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"correspondents":[],"tags":[3],"document_types":[1]}
|
1
src-ui/cypress/fixtures/documents/documents.json
Normal file
1
src-ui/cypress/fixtures/documents/documents.json
Normal file
File diff suppressed because one or more lines are too long
BIN
src-ui/cypress/fixtures/documents/lorem-ipsum.png
Normal file
BIN
src-ui/cypress/fixtures/documents/lorem-ipsum.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
1
src-ui/cypress/fixtures/saved_views/savedviews.json
Normal file
1
src-ui/cypress/fixtures/saved_views/savedviews.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]}
|
1
src-ui/cypress/fixtures/tags/tags.json
Normal file
1
src-ui/cypress/fixtures/tags/tags.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"count":8,"next":null,"previous":null,"results":[{"id":4,"slug":"another-sample-tag","name":"Another Sample Tag","color":"#a6cee3","text_color":"#000000","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":7,"slug":"newone","name":"NewOne","color":"#9e4ad1","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":6,"slug":"partial-tag","name":"Partial Tag","color":"#72dba7","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":1},{"id":2,"slug":"tag-2","name":"Tag 2","color":"#612db7","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":3,"slug":"tag-3","name":"Tag 3","color":"#b2df8a","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4},{"id":5,"slug":"tagwithpartial","name":"TagWithPartial","color":"#3b2db4","text_color":"#ffffff","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":8,"slug":"test-another","name":"Test Another","color":"#3ccea5","text_color":"#000000","match":"","matching_algorithm":4,"is_insensitive":true,"is_inbox_tag":false,"document_count":0},{"id":1,"slug":"test-tag","name":"Test Tag","color":"#fb9a99","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4}]}
|
64
src-ui/cypress/integration/document-detail.spec.ts
Normal file
64
src-ui/cypress/integration/document-detail.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
describe('document-detail', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.modifiedDocuments = []
|
||||||
|
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
response = response.results.find((d) => d.id == 1)
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('PUT', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||||
|
this.modifiedDocuments.push(req.body) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}).as('saveDoc')
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/metadata/', {
|
||||||
|
fixture: 'documents/1/metadata.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/suggestions/', {
|
||||||
|
fixture: 'documents/1/suggestions.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/saved_views/*', {
|
||||||
|
fixture: 'saved_views/savedviews.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||||
|
fixture: 'tags/tags.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||||
|
fixture: 'correspondents/correspondents.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/document_types/*', {
|
||||||
|
fixture: 'document_types/doctypes.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.viewport(1024, 1024)
|
||||||
|
cy.visit('/documents/1/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should activate / deactivate save button when changes are saved', () => {
|
||||||
|
cy.contains('button', 'Save').should('be.disabled')
|
||||||
|
cy.get('app-input-text[formcontrolname="title"]')
|
||||||
|
.type(' additional')
|
||||||
|
.wait(1500) // this delay is for frontend debounce
|
||||||
|
cy.contains('button', 'Save').should('not.be.disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should warn on unsaved changes', () => {
|
||||||
|
cy.get('app-input-text[formcontrolname="title"]')
|
||||||
|
.type(' additional')
|
||||||
|
.wait(1500) // this delay is for frontend debounce
|
||||||
|
cy.get('button[title="Close"]').click()
|
||||||
|
cy.contains('You have unsaved changes')
|
||||||
|
cy.contains('button', 'Cancel').click().wait(150)
|
||||||
|
cy.contains('button', 'Save').click().wait('@saveDoc').wait(2000) // navigates away after saving
|
||||||
|
cy.contains('You have unsaved changes').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
143
src-ui/cypress/integration/documents-list.spec.ts
Normal file
143
src-ui/cypress/integration/documents-list.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
describe('documents-list', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.bulkEdits = {}
|
||||||
|
|
||||||
|
// mock API methods
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
// bulk edit
|
||||||
|
cy.intercept(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8000/api/documents/bulk_edit/',
|
||||||
|
(req) => {
|
||||||
|
this.bulkEdits = req.body // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
|
||||||
|
// bulkEdits was set earlier by bulk_edit intercept
|
||||||
|
if (this.bulkEdits.hasOwnProperty('documents')) {
|
||||||
|
response.results = response.results.map((d) => {
|
||||||
|
if ((this.bulkEdits['documents'] as Array<number>).includes(d.id)) {
|
||||||
|
switch (this.bulkEdits['method']) {
|
||||||
|
case 'modify_tags':
|
||||||
|
d.tags = (d.tags as Array<number>).concat([
|
||||||
|
this.bulkEdits['parameters']['add_tags'],
|
||||||
|
])
|
||||||
|
break
|
||||||
|
case 'set_correspondent':
|
||||||
|
d.correspondent =
|
||||||
|
this.bulkEdits['parameters']['correspondent']
|
||||||
|
break
|
||||||
|
case 'set_document_type':
|
||||||
|
d.document_type =
|
||||||
|
this.bulkEdits['parameters']['document_type']
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
})
|
||||||
|
} else if (req.query.hasOwnProperty('tags__id__all')) {
|
||||||
|
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&tags__id__all=2
|
||||||
|
const tag_id = +req.query['tags__id__all']
|
||||||
|
response.results = (documentsJson.results as Array<any>).filter((d) =>
|
||||||
|
(d.tags as Array<number>).includes(tag_id)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/thumb/', {
|
||||||
|
fixture: 'documents/lorem-ipsum.png',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||||
|
fixture: 'tags/tags.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||||
|
fixture: 'correspondents/correspondents.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/document_types/*', {
|
||||||
|
fixture: 'document_types/doctypes.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit('/documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents rendered as cards with thumbnails', () => {
|
||||||
|
cy.contains('3 documents')
|
||||||
|
cy.contains('lorem-ipsum')
|
||||||
|
cy.get('app-document-card-small:first-of-type img')
|
||||||
|
.invoke('attr', 'src')
|
||||||
|
.should('eq', 'http://localhost:8000/api/documents/1/thumb/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should change to table "details" view', () => {
|
||||||
|
cy.get('div.btn-group-toggle input[value="details"]').parent().click()
|
||||||
|
cy.get('table')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should change to large cards view', () => {
|
||||||
|
cy.get('div.btn-group-toggle input[value="largeCards"]').parent().click()
|
||||||
|
cy.get('app-document-card-large')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter tags', () => {
|
||||||
|
cy.get('app-filter-editor app-filterable-dropdown[title="Tags"]').within(
|
||||||
|
() => {
|
||||||
|
cy.contains('button', 'Tags').click()
|
||||||
|
cy.contains('button', 'Tag 2').click()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply tags', () => {
|
||||||
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
|
cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within(
|
||||||
|
() => {
|
||||||
|
cy.contains('button', 'Tags').click()
|
||||||
|
cy.contains('button', 'Test Tag').click()
|
||||||
|
cy.contains('button', 'Apply').click()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cy.contains('button', 'Confirm').click()
|
||||||
|
cy.get('app-document-card-small:first-of-type').contains('Test Tag')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply correspondent', () => {
|
||||||
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
|
cy.get(
|
||||||
|
'app-bulk-editor app-filterable-dropdown[title="Correspondent"]'
|
||||||
|
).within(() => {
|
||||||
|
cy.contains('button', 'Correspondent').click()
|
||||||
|
cy.contains('button', 'ABC Test Correspondent').click()
|
||||||
|
cy.contains('button', 'Apply').click()
|
||||||
|
})
|
||||||
|
cy.contains('button', 'Confirm').click()
|
||||||
|
cy.get('app-document-card-small:first-of-type').contains(
|
||||||
|
'ABC Test Correspondent'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply document type', () => {
|
||||||
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
|
cy.get(
|
||||||
|
'app-bulk-editor app-filterable-dropdown[title="Document type"]'
|
||||||
|
).within(() => {
|
||||||
|
cy.contains('button', 'Document type').click()
|
||||||
|
cy.contains('button', 'Test Doc Type').click()
|
||||||
|
cy.contains('button', 'Apply').click()
|
||||||
|
})
|
||||||
|
cy.contains('button', 'Confirm').click()
|
||||||
|
cy.get('app-document-card-small:first-of-type').contains('Test Doc Type')
|
||||||
|
})
|
||||||
|
})
|
32
src-ui/cypress/integration/manage.spec.ts
Normal file
32
src-ui/cypress/integration/manage.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
describe('manage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||||
|
fixture: 'correspondents/correspondents.json',
|
||||||
|
})
|
||||||
|
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||||
|
fixture: 'tags/tags.json',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of correspondents with bottom pagination as well', () => {
|
||||||
|
cy.visit('/correspondents')
|
||||||
|
cy.get('tbody').find('tr').its('length').should('eq', 25)
|
||||||
|
cy.get('ngb-pagination').its('length').should('eq', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of tags without bottom pagination', () => {
|
||||||
|
cy.visit('/tags')
|
||||||
|
cy.get('tbody').find('tr').its('length').should('eq', 8)
|
||||||
|
cy.get('ngb-pagination').its('length').should('eq', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by tag', () => {
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/*', (req) => {
|
||||||
|
if (req.url.indexOf('tags__id__all=4'))
|
||||||
|
req.reply({ count: 3, next: null, previous: null, results: [] })
|
||||||
|
})
|
||||||
|
cy.visit('/tags')
|
||||||
|
cy.get('tbody').find('button').contains('Documents').first().click() // id = 4
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
})
|
91
src-ui/cypress/integration/settings.spec.ts
Normal file
91
src-ui/cypress/integration/settings.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
describe('settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.modifiedViews = []
|
||||||
|
|
||||||
|
// mock API methods
|
||||||
|
cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => {
|
||||||
|
// saved views PATCH
|
||||||
|
cy.intercept(
|
||||||
|
'PATCH',
|
||||||
|
'http://localhost:8000/api/saved_views/*',
|
||||||
|
(req) => {
|
||||||
|
this.modifiedViews.push(req.body) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/saved_views/*', (req) => {
|
||||||
|
let response = { ...savedViewsJson }
|
||||||
|
if (this.modifiedViews.length) {
|
||||||
|
response.results = response.results.map((v) => {
|
||||||
|
if (this.modifiedViews.find((mv) => mv.id == v.id))
|
||||||
|
v = this.modifiedViews.find((mv) => mv.id == v.id)
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
}).as('savedViews')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
response = response.results.find((d) => d.id == 1)
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/metadata/', {
|
||||||
|
fixture: 'documents/1/metadata.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/suggestions/', {
|
||||||
|
fixture: 'documents/1/suggestions.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.viewport(1024, 1024)
|
||||||
|
cy.visit('/settings')
|
||||||
|
cy.wait('@savedViews')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should activate / deactivate save button when settings change and are saved', () => {
|
||||||
|
cy.contains('button', 'Save').should('be.disabled')
|
||||||
|
cy.contains('Use system settings').click()
|
||||||
|
cy.contains('button', 'Save').should('not.be.disabled')
|
||||||
|
cy.contains('button', 'Save').click()
|
||||||
|
cy.contains('button', 'Save').should('be.disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should warn on unsaved changes', () => {
|
||||||
|
cy.contains('Use system settings').click()
|
||||||
|
cy.contains('a', 'Dashboard').click()
|
||||||
|
cy.contains('You have unsaved changes')
|
||||||
|
cy.contains('button', 'Cancel').click()
|
||||||
|
cy.contains('button', 'Save').click().wait('@savedViews')
|
||||||
|
cy.contains('a', 'Dashboard').click()
|
||||||
|
cy.contains('You have unsaved changes').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply appearance changes when set', () => {
|
||||||
|
cy.contains('Use system settings').click()
|
||||||
|
cy.get('body').should('not.have.class', 'color-scheme-system')
|
||||||
|
cy.contains('Enable dark mode').click()
|
||||||
|
cy.get('body').should('have.class', 'color-scheme-dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove saved view from sidebar when unset', () => {
|
||||||
|
cy.contains('a', 'Saved views').click()
|
||||||
|
cy.get('#show_in_sidebar_1').click()
|
||||||
|
cy.contains('button', 'Save').click().wait('@savedViews')
|
||||||
|
cy.contains('li', 'Inbox').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove saved view from dashboard when unset', () => {
|
||||||
|
cy.contains('a', 'Saved views').click()
|
||||||
|
cy.get('#show_on_dashboard_1').click()
|
||||||
|
cy.contains('button', 'Save').click().wait('@savedViews')
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.get('app-saved-view-widget').contains('Inbox').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
3
src-ui/cypress/plugins/index.ts
Normal file
3
src-ui/cypress/plugins/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||||
|
// For more info, visit https://on.cypress.io/plugins-api
|
||||||
|
module.exports = (on, config) => {}
|
43
src-ui/cypress/support/commands.ts
Normal file
43
src-ui/cypress/support/commands.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// This example namespace declaration will help
|
||||||
|
// with Intellisense and code completion in your
|
||||||
|
// IDE or Text Editor.
|
||||||
|
// ***********************************************
|
||||||
|
// declare namespace Cypress {
|
||||||
|
// interface Chainable<Subject = any> {
|
||||||
|
// customCommand(param: any): typeof customCommand;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function customCommand(param: any): void {
|
||||||
|
// console.warn(param);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// NOTE: You can use it like so:
|
||||||
|
// Cypress.Commands.add('customCommand', customCommand);
|
||||||
|
//
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
17
src-ui/cypress/support/index.ts
Normal file
17
src-ui/cypress/support/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||||
|
// import './commands';
|
8
src-ui/cypress/tsconfig.json
Normal file
8
src-ui/cypress/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": false,
|
||||||
|
"types": ["cypress"]
|
||||||
|
}
|
||||||
|
}
|
@@ -2,18 +2,16 @@
|
|||||||
// Protractor configuration file, see link for more information
|
// Protractor configuration file, see link for more information
|
||||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type { import("protractor").Config }
|
* @type { import("protractor").Config }
|
||||||
*/
|
*/
|
||||||
exports.config = {
|
exports.config = {
|
||||||
allScriptsTimeout: 11000,
|
allScriptsTimeout: 11000,
|
||||||
specs: [
|
specs: ['./src/**/*.e2e-spec.ts'],
|
||||||
'./src/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
capabilities: {
|
capabilities: {
|
||||||
browserName: 'chrome'
|
browserName: 'chrome',
|
||||||
},
|
},
|
||||||
directConnect: true,
|
directConnect: true,
|
||||||
baseUrl: 'http://localhost:4200/',
|
baseUrl: 'http://localhost:4200/',
|
||||||
@@ -21,16 +19,18 @@ exports.config = {
|
|||||||
jasmineNodeOpts: {
|
jasmineNodeOpts: {
|
||||||
showColors: true,
|
showColors: true,
|
||||||
defaultTimeoutInterval: 30000,
|
defaultTimeoutInterval: 30000,
|
||||||
print: function() {}
|
print: function () {},
|
||||||
},
|
},
|
||||||
onPrepare() {
|
onPrepare() {
|
||||||
require('ts-node').register({
|
require('ts-node').register({
|
||||||
project: require('path').join(__dirname, './tsconfig.json')
|
project: require('path').join(__dirname, './tsconfig.json'),
|
||||||
});
|
})
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({
|
jasmine.getEnv().addReporter(
|
||||||
spec: {
|
new SpecReporter({
|
||||||
displayStacktrace: StacktraceOption.PRETTY
|
spec: {
|
||||||
}
|
displayStacktrace: StacktraceOption.PRETTY,
|
||||||
}));
|
},
|
||||||
}
|
})
|
||||||
};
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@@ -1,23 +1,25 @@
|
|||||||
import { AppPage } from './app.po';
|
import { AppPage } from './app.po'
|
||||||
import { browser, logging } from 'protractor';
|
import { browser, logging } from 'protractor'
|
||||||
|
|
||||||
describe('workspace-project App', () => {
|
describe('workspace-project App', () => {
|
||||||
let page: AppPage;
|
let page: AppPage
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
page = new AppPage();
|
page = new AppPage()
|
||||||
});
|
})
|
||||||
|
|
||||||
it('should display welcome message', () => {
|
it('should display welcome message', () => {
|
||||||
page.navigateTo();
|
page.navigateTo()
|
||||||
expect(page.getTitleText()).toEqual('paperless-ui app is running!');
|
expect(page.getTitleText()).toEqual('paperless-ui app is running!')
|
||||||
});
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// Assert that there are no errors emitted from the browser
|
// Assert that there are no errors emitted from the browser
|
||||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
const logs = await browser.manage().logs().get(logging.Type.BROWSER)
|
||||||
expect(logs).not.toContain(jasmine.objectContaining({
|
expect(logs).not.toContain(
|
||||||
level: logging.Level.SEVERE,
|
jasmine.objectContaining({
|
||||||
} as logging.Entry));
|
level: logging.Level.SEVERE,
|
||||||
});
|
} as logging.Entry)
|
||||||
});
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
import { browser, by, element } from 'protractor';
|
import { browser, by, element } from 'protractor'
|
||||||
|
|
||||||
export class AppPage {
|
export class AppPage {
|
||||||
navigateTo(): Promise<unknown> {
|
navigateTo(): Promise<unknown> {
|
||||||
return browser.get(browser.baseUrl) as Promise<unknown>;
|
return browser.get(browser.baseUrl) as Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitleText(): Promise<string> {
|
getTitleText(): Promise<string> {
|
||||||
return element(by.css('app-root .content span')).getText() as Promise<string>;
|
return element(
|
||||||
|
by.css('app-root .content span')
|
||||||
|
).getText() as Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
src-ui/jest.config.js
Normal file
8
src-ui/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
moduleNameMapper: {
|
||||||
|
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
||||||
|
},
|
||||||
|
preset: 'jest-preset-angular',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||||
|
testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
|
||||||
|
}
|
@@ -1,32 +0,0 @@
|
|||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage-istanbul-reporter'),
|
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
|
||||||
client: {
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
|
||||||
dir: require('path').join(__dirname, './coverage/paperless-ui'),
|
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
|
||||||
fixWebpackSourcePaths: true
|
|
||||||
},
|
|
||||||
reporters: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
singleRun: false,
|
|
||||||
restartOnFileChange: true
|
|
||||||
});
|
|
||||||
};
|
|
15205
src-ui/package-lock.json
generated
15205
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,53 +7,52 @@
|
|||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e",
|
||||||
|
"cy:run": "cypress run",
|
||||||
|
"e2e:ci": "concurrently 'npm run start' 'wait-on http-get://localhost:4200 && npm run cy:run' --kill-others --success first"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~13.2.4",
|
"@angular/common": "~13.3.1",
|
||||||
"@angular/common": "~13.2.4",
|
"@angular/compiler": "~13.3.1",
|
||||||
"@angular/compiler": "~13.2.4",
|
"@angular/core": "~13.3.1",
|
||||||
"@angular/core": "~13.2.4",
|
"@angular/forms": "~13.3.1",
|
||||||
"@angular/forms": "~13.2.4",
|
"@angular/localize": "~13.3.1",
|
||||||
"@angular/localize": "~13.2.4",
|
"@angular/platform-browser": "~13.3.1",
|
||||||
"@angular/platform-browser": "~13.2.4",
|
"@angular/platform-browser-dynamic": "~13.3.1",
|
||||||
"@angular/platform-browser-dynamic": "~13.2.4",
|
"@angular/router": "~13.3.1",
|
||||||
"@angular/router": "~13.2.4",
|
"@ng-bootstrap/ng-bootstrap": "^12.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
|
|
||||||
"@ng-select/ng-select": "^8.1.1",
|
"@ng-select/ng-select": "^8.1.1",
|
||||||
"@ngneat/dirty-check-forms": "^1.1.0",
|
"@ngneat/dirty-check-forms": "^3.0.2",
|
||||||
"@popperjs/core": "^2.11.2",
|
"@popperjs/core": "^2.11.4",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"ng2-pdf-viewer": "^8.0.1",
|
"ng2-pdf-viewer": "^9.0.0",
|
||||||
"ngx-color": "^7.3.3",
|
"ngx-color": "^7.3.3",
|
||||||
"ngx-cookie-service": "^13.1.2",
|
"ngx-cookie-service": "^13.1.2",
|
||||||
"ngx-file-drop": "^13.0.0",
|
"ngx-file-drop": "^13.0.0",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"rxjs": "~7.5.5",
|
||||||
"rxjs": "~6.6.7",
|
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"uuid": "^8.3.1",
|
"uuid": "^8.3.1",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~13.2.5",
|
"@angular-builders/jest": "13.0.3",
|
||||||
"@angular/cli": "~13.2.5",
|
"@angular-devkit/build-angular": "~13.3.1",
|
||||||
"@angular/compiler-cli": "~13.2.4",
|
"@angular/cli": "~13.3.1",
|
||||||
"@types/jasmine": "~3.10.3",
|
"@angular/compiler-cli": "~13.3.1",
|
||||||
"@types/jasminewd2": "~2.0.10",
|
"@types/jest": "27.4.1",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.23",
|
||||||
"codelyzer": "^6.0.2",
|
"codelyzer": "^6.0.2",
|
||||||
"jasmine-core": "~4.0.1",
|
"concurrently": "7.0.0",
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
"jest": "27.5.1",
|
||||||
"karma": "~6.3.16",
|
"ts-node": "~10.7.0",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
|
||||||
"karma-jasmine": "~4.0.1",
|
|
||||||
"karma-jasmine-html-reporter": "^1.7.0",
|
|
||||||
"protractor": "~7.0.0",
|
|
||||||
"ts-node": "~10.5.0",
|
|
||||||
"tslint": "~6.1.3",
|
"tslint": "~6.1.3",
|
||||||
"typescript": "~4.5.5"
|
"typescript": "~4.6.3",
|
||||||
|
"wait-on": "~6.0.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cypress": "~9.5.3",
|
||||||
|
"@cypress/schematic": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
src-ui/setup-jest.ts
Normal file
30
src-ui/setup-jest.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'jest-preset-angular/setup-jest'
|
||||||
|
|
||||||
|
/* global mocks for jsdom */
|
||||||
|
const mock = () => {
|
||||||
|
let storage: { [key: string]: string } = {}
|
||||||
|
return {
|
||||||
|
getItem: (key: string) => (key in storage ? storage[key] : null),
|
||||||
|
setItem: (key: string, value: string) => (storage[key] = value || ''),
|
||||||
|
removeItem: (key: string) => delete storage[key],
|
||||||
|
clear: () => (storage = {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: mock() })
|
||||||
|
Object.defineProperty(window, 'sessionStorage', { value: mock() })
|
||||||
|
Object.defineProperty(window, 'getComputedStyle', {
|
||||||
|
value: () => ['-webkit-appearance'],
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(document.body.style, 'transform', {
|
||||||
|
value: () => {
|
||||||
|
return {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/* output shorter and more meaningful Zone error stack traces */
|
||||||
|
// Error.stackTraceLimit = 2
|
@@ -1,39 +1,47 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core'
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component';
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component';
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component';
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component';
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { LogsComponent } from './components/manage/logs/logs.component';
|
import { LogsComponent } from './components/manage/logs/logs.component'
|
||||||
import { SettingsComponent } from './components/manage/settings/settings.component';
|
import { SettingsComponent } from './components/manage/settings/settings.component'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import {DocumentAsnComponent} from "./components/document-asn/document-asn.component";
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DirtyFormGuard } from './guards/dirty-form.guard';
|
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', redirectTo: 'dashboard', pathMatch: 'full'},
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
{path: '', component: AppFrameComponent, children: [
|
{
|
||||||
{path: 'dashboard', component: DashboardComponent },
|
path: '',
|
||||||
{path: 'documents', component: DocumentListComponent },
|
component: AppFrameComponent,
|
||||||
{path: 'view/:id', component: DocumentListComponent },
|
children: [
|
||||||
{path: 'documents/:id', component: DocumentDetailComponent },
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
{path: 'asn/:id', component: DocumentAsnComponent },
|
{ path: 'documents', component: DocumentListComponent },
|
||||||
{path: 'tags', component: TagListComponent },
|
{ path: 'view/:id', component: DocumentListComponent },
|
||||||
{path: 'documenttypes', component: DocumentTypeListComponent },
|
{ path: 'documents/:id', component: DocumentDetailComponent },
|
||||||
{path: 'correspondents', component: CorrespondentListComponent },
|
{ path: 'asn/:id', component: DocumentAsnComponent },
|
||||||
{path: 'logs', component: LogsComponent },
|
{ path: 'tags', component: TagListComponent },
|
||||||
{path: 'settings', component: SettingsComponent, canDeactivate: [DirtyFormGuard] },
|
{ path: 'documenttypes', component: DocumentTypeListComponent },
|
||||||
]},
|
{ path: 'correspondents', component: CorrespondentListComponent },
|
||||||
|
{ path: 'logs', component: LogsComponent },
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
component: SettingsComponent,
|
||||||
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
{path: '404', component: NotFoundComponent},
|
{ path: '404', component: NotFoundComponent },
|
||||||
{path: '**', redirectTo: '/404', pathMatch: 'full'}
|
{ path: '**', redirectTo: '/404', pathMatch: 'full' },
|
||||||
];
|
]
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
|
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule {}
|
||||||
|
@@ -1,35 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
RouterTestingModule
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the app', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should have as title 'paperless-ui'`, () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app.title).toEqual('paperless-ui');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render title', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.nativeElement;
|
|
||||||
expect(compiled.querySelector('.content span').textContent).toContain('paperless-ui app is running!');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,25 +1,29 @@
|
|||||||
import { SettingsService, SETTINGS_KEYS } from './services/settings.service';
|
import { SettingsService, SETTINGS_KEYS } from './services/settings.service'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router'
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs'
|
||||||
import { ConsumerStatusService } from './services/consumer-status.service';
|
import { ConsumerStatusService } from './services/consumer-status.service'
|
||||||
import { ToastService } from './services/toast.service';
|
import { ToastService } from './services/toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
newDocumentSubscription: Subscription
|
||||||
|
successSubscription: Subscription
|
||||||
|
failedSubscription: Subscription
|
||||||
|
|
||||||
newDocumentSubscription: Subscription;
|
constructor(
|
||||||
successSubscription: Subscription;
|
private settings: SettingsService,
|
||||||
failedSubscription: Subscription;
|
private consumerStatusService: ConsumerStatusService,
|
||||||
|
private toastService: ToastService,
|
||||||
constructor (private settings: SettingsService, private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router) {
|
private router: Router
|
||||||
let anyWindow = (window as any)
|
) {
|
||||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js';
|
let anyWindow = window as any
|
||||||
this.settings.updateDarkModeSettings()
|
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||||
|
this.settings.updateAppearanceSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -36,7 +40,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showNotification(key) {
|
private showNotification(key) {
|
||||||
if (this.router.url == '/dashboard' && this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD)) {
|
if (
|
||||||
|
this.router.url == '/dashboard' &&
|
||||||
|
this.settings.get(
|
||||||
|
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
|
||||||
|
)
|
||||||
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.settings.get(key)
|
return this.settings.get(key)
|
||||||
@@ -45,26 +54,50 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.consumerStatusService.connect()
|
this.consumerStatusService.connect()
|
||||||
|
|
||||||
|
this.successSubscription = this.consumerStatusService
|
||||||
|
.onDocumentConsumptionFinished()
|
||||||
|
.subscribe((status) => {
|
||||||
|
if (
|
||||||
|
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
|
||||||
|
) {
|
||||||
|
this.toastService.show({
|
||||||
|
title: $localize`Document added`,
|
||||||
|
delay: 10000,
|
||||||
|
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||||
|
actionName: $localize`Open document`,
|
||||||
|
action: () => {
|
||||||
|
this.router.navigate(['documents', status.documentId])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.successSubscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => {
|
this.failedSubscription = this.consumerStatusService
|
||||||
if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)) {
|
.onDocumentConsumptionFailed()
|
||||||
this.toastService.show({title: $localize`Document added`, delay: 10000, content: $localize`Document ${status.filename} was added to paperless.`, actionName: $localize`Open document`, action: () => {
|
.subscribe((status) => {
|
||||||
this.router.navigate(['documents', status.documentId])
|
if (
|
||||||
}})
|
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)
|
||||||
}
|
) {
|
||||||
})
|
this.toastService.showError(
|
||||||
|
$localize`Could not add ${status.filename}\: ${status.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.failedSubscription = this.consumerStatusService.onDocumentConsumptionFailed().subscribe(status => {
|
this.newDocumentSubscription = this.consumerStatusService
|
||||||
if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)) {
|
.onDocumentDetected()
|
||||||
this.toastService.showError($localize`Could not add ${status.filename}\: ${status.message}`)
|
.subscribe((status) => {
|
||||||
}
|
if (
|
||||||
})
|
this.showNotification(
|
||||||
|
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
|
||||||
this.newDocumentSubscription = this.consumerStatusService.onDocumentDetected().subscribe(status => {
|
)
|
||||||
if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT)) {
|
) {
|
||||||
this.toastService.show({title: $localize`New document detected`, delay: 5000, content: $localize`Document ${status.filename} is being processed by paperless.`})
|
this.toastService.show({
|
||||||
}
|
title: $localize`New document detected`,
|
||||||
})
|
delay: 5000,
|
||||||
|
content: $localize`Document ${status.filename} is being processed by paperless.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,86 +1,94 @@
|
|||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core'
|
||||||
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppComponent } from './app.component'
|
||||||
import { AppComponent } from './app.component';
|
import {
|
||||||
import { NgbDateAdapter, NgbDateParserFormatter, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
NgbDateAdapter,
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
NgbDateParserFormatter,
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component';
|
NgbModule,
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component';
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component';
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { LogsComponent } from './components/manage/logs/logs.component';
|
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
||||||
import { SettingsComponent } from './components/manage/settings/settings.component';
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { DatePipe, registerLocaleData } from '@angular/common';
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
import { LogsComponent } from './components/manage/logs/logs.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component';
|
import { SettingsComponent } from './components/manage/settings/settings.component'
|
||||||
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { CorrespondentEditDialogComponent } from './components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
|
import { DatePipe, registerLocaleData } from '@angular/common'
|
||||||
import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
|
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
|
||||||
import { TagComponent } from './components/common/tag/tag.component';
|
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from './components/common/page-header/page-header.component';
|
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component';
|
import { TagComponent } from './components/common/tag/tag.component'
|
||||||
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component';
|
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
||||||
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component';
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component';
|
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
||||||
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component';
|
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
|
||||||
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component';
|
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component';
|
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
|
||||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
|
||||||
import { TextComponent } from './components/common/input/text/text.component';
|
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
|
||||||
import { SelectComponent } from './components/common/input/select/select.component';
|
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
|
||||||
import { CheckComponent } from './components/common/input/check/check.component';
|
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component';
|
import { TextComponent } from './components/common/input/text/text.component'
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { SelectComponent } from './components/common/input/select/select.component'
|
||||||
import { TagsComponent } from './components/common/input/tags/tags.component';
|
import { CheckComponent } from './components/common/input/check/check.component'
|
||||||
import { SortableDirective } from './directives/sortable.directive';
|
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import { TagsComponent } from './components/common/input/tags/tags.component'
|
||||||
import { CsrfInterceptor } from './interceptors/csrf.interceptor';
|
import { SortableDirective } from './directives/sortable.directive'
|
||||||
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component';
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component';
|
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
||||||
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component';
|
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
|
||||||
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component';
|
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
|
||||||
import { PdfViewerModule } from 'ng2-pdf-viewer';
|
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
|
||||||
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
|
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
|
||||||
import { YesNoPipe } from './pipes/yes-no.pipe';
|
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
import { FileSizePipe } from './pipes/file-size.pipe';
|
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
|
||||||
import { FilterPipe } from './pipes/filter.pipe';
|
import { YesNoPipe } from './pipes/yes-no.pipe'
|
||||||
import { DocumentTitlePipe } from './pipes/document-title.pipe';
|
import { FileSizePipe } from './pipes/file-size.pipe'
|
||||||
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
|
import { FilterPipe } from './pipes/filter.pipe'
|
||||||
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
|
import { DocumentTitlePipe } from './pipes/document-title.pipe'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
|
||||||
import { NumberComponent } from './components/common/input/number/number.component';
|
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
|
||||||
import { SafePipe } from './pipes/safe.pipe';
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { CustomDatePipe } from './pipes/custom-date.pipe';
|
import { NumberComponent } from './components/common/input/number/number.component'
|
||||||
import { DateComponent } from './components/common/input/date/date.component';
|
import { SafeUrlPipe } from './pipes/safeurl.pipe'
|
||||||
import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter';
|
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
|
||||||
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter';
|
import { CustomDatePipe } from './pipes/custom-date.pipe'
|
||||||
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor';
|
import { DateComponent } from './components/common/input/date/date.component'
|
||||||
import { ColorSliderModule } from 'ngx-color/slider';
|
import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter'
|
||||||
import { ColorComponent } from './components/common/input/color/color.component';
|
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component';
|
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
|
||||||
|
import { ColorSliderModule } from 'ngx-color/slider'
|
||||||
import localeCs from '@angular/common/locales/cs';
|
import { ColorComponent } from './components/common/input/color/color.component'
|
||||||
import localeDa from '@angular/common/locales/da';
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import localeDe from '@angular/common/locales/de';
|
|
||||||
import localeEnGb from '@angular/common/locales/en-GB';
|
|
||||||
import localeEs from '@angular/common/locales/es';
|
|
||||||
import localeFr from '@angular/common/locales/fr';
|
|
||||||
import localeIt from '@angular/common/locales/it';
|
|
||||||
import localeLb from '@angular/common/locales/lb';
|
|
||||||
import localeNl from '@angular/common/locales/nl';
|
|
||||||
import localePl from '@angular/common/locales/pl';
|
|
||||||
import localePt from '@angular/common/locales/pt';
|
|
||||||
import localeSv from '@angular/common/locales/sv';
|
|
||||||
import localeRo from '@angular/common/locales/ro';
|
|
||||||
import localeRu from '@angular/common/locales/ru';
|
|
||||||
|
|
||||||
|
import localeBe from '@angular/common/locales/be'
|
||||||
|
import localeCs from '@angular/common/locales/cs'
|
||||||
|
import localeDa from '@angular/common/locales/da'
|
||||||
|
import localeDe from '@angular/common/locales/de'
|
||||||
|
import localeEnGb from '@angular/common/locales/en-GB'
|
||||||
|
import localeEs from '@angular/common/locales/es'
|
||||||
|
import localeFr from '@angular/common/locales/fr'
|
||||||
|
import localeIt from '@angular/common/locales/it'
|
||||||
|
import localeLb from '@angular/common/locales/lb'
|
||||||
|
import localeNl from '@angular/common/locales/nl'
|
||||||
|
import localePl from '@angular/common/locales/pl'
|
||||||
|
import localePt from '@angular/common/locales/pt'
|
||||||
|
import localeRo from '@angular/common/locales/ro'
|
||||||
|
import localeRu from '@angular/common/locales/ru'
|
||||||
|
import localeSl from '@angular/common/locales/sl'
|
||||||
|
import localeSr from '@angular/common/locales/sr'
|
||||||
|
import localeSv from '@angular/common/locales/sv'
|
||||||
|
import localeTr from '@angular/common/locales/tr'
|
||||||
|
import localeZh from '@angular/common/locales/zh'
|
||||||
|
|
||||||
|
registerLocaleData(localeBe)
|
||||||
registerLocaleData(localeCs)
|
registerLocaleData(localeCs)
|
||||||
registerLocaleData(localeDa)
|
registerLocaleData(localeDa)
|
||||||
registerLocaleData(localeDe)
|
registerLocaleData(localeDe)
|
||||||
@@ -91,11 +99,15 @@ registerLocaleData(localeIt)
|
|||||||
registerLocaleData(localeLb)
|
registerLocaleData(localeLb)
|
||||||
registerLocaleData(localeNl)
|
registerLocaleData(localeNl)
|
||||||
registerLocaleData(localePl)
|
registerLocaleData(localePl)
|
||||||
registerLocaleData(localePt, "pt-BR")
|
registerLocaleData(localePt, 'pt-BR')
|
||||||
registerLocaleData(localePt, "pt-PT")
|
registerLocaleData(localePt, 'pt-PT')
|
||||||
registerLocaleData(localeRo)
|
registerLocaleData(localeRo)
|
||||||
registerLocaleData(localeRu)
|
registerLocaleData(localeRu)
|
||||||
|
registerLocaleData(localeSl)
|
||||||
|
registerLocaleData(localeSr)
|
||||||
registerLocaleData(localeSv)
|
registerLocaleData(localeSv)
|
||||||
|
registerLocaleData(localeTr)
|
||||||
|
registerLocaleData(localeZh)
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -104,8 +116,8 @@ registerLocaleData(localeSv)
|
|||||||
DocumentDetailComponent,
|
DocumentDetailComponent,
|
||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
TagListComponent,
|
TagListComponent,
|
||||||
CorrespondentListComponent,
|
|
||||||
DocumentTypeListComponent,
|
DocumentTypeListComponent,
|
||||||
|
CorrespondentListComponent,
|
||||||
LogsComponent,
|
LogsComponent,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
@@ -142,11 +154,12 @@ registerLocaleData(localeSv)
|
|||||||
MetadataCollapseComponent,
|
MetadataCollapseComponent,
|
||||||
SelectDialogComponent,
|
SelectDialogComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
SafePipe,
|
SafeUrlPipe,
|
||||||
|
SafeHtmlPipe,
|
||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
DateComponent,
|
DateComponent,
|
||||||
ColorComponent,
|
ColorComponent,
|
||||||
DocumentAsnComponent
|
DocumentAsnComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -156,27 +169,28 @@ registerLocaleData(localeSv)
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxFileDropModule,
|
NgxFileDropModule,
|
||||||
InfiniteScrollModule,
|
|
||||||
PdfViewerModule,
|
PdfViewerModule,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
ColorSliderModule
|
ColorSliderModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DatePipe,
|
DatePipe,
|
||||||
CookieService, {
|
CookieService,
|
||||||
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
useClass: CsrfInterceptor,
|
useClass: CsrfInterceptor,
|
||||||
multi: true
|
multi: true,
|
||||||
},{
|
},
|
||||||
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
useClass: ApiVersionInterceptor,
|
useClass: ApiVersionInterceptor,
|
||||||
multi: true
|
multi: true,
|
||||||
},
|
},
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
{provide: NgbDateAdapter, useClass: ISODateTimeAdapter},
|
{ provide: NgbDateAdapter, useClass: ISODateTimeAdapter },
|
||||||
{provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter}
|
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
@@ -62,7 +62,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||||
</svg> <ng-container i18n>Documents</ng-container>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||||
</svg> {{d.title | documentTitle}}
|
</svg> {{d.title | documentTitle}}
|
||||||
<span class="close bg-light" (click)="closeDocument(d); $event.preventDefault()">
|
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<div class="d-flex w-100 flex-wrap">
|
<div class="d-flex w-100 flex-wrap">
|
||||||
<a class="nav-link pe-0 pb-0" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx">
|
<a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16">
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||||
</svg> <ng-container i18n>GitHub</ng-container>
|
</svg> <ng-container i18n>GitHub</ng-container>
|
||||||
|
@@ -35,16 +35,14 @@
|
|||||||
|
|
||||||
.sidebar .nav-link {
|
.sidebar .nav-link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link .sidebaricon {
|
.sidebar .nav-link .sidebaricon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link.active {
|
.sidebar .nav-link.active {
|
||||||
color: $primary;
|
color: var(--bs-primary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +170,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: #fff;
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
color: #212529;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { AppFrameComponent } from './app-frame.component';
|
|
||||||
|
|
||||||
describe('AppFrameComponent', () => {
|
|
||||||
let component: AppFrameComponent;
|
|
||||||
let fixture: ComponentFixture<AppFrameComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ AppFrameComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(AppFrameComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,26 +1,31 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core'
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms'
|
||||||
import { ActivatedRoute, Router, Params } from '@angular/router';
|
import { ActivatedRoute, Router, Params } from '@angular/router'
|
||||||
import { from, Observable, Subscription, BehaviorSubject } from 'rxjs';
|
import { from, Observable, Subscription, BehaviorSubject } from 'rxjs'
|
||||||
import { debounceTime, distinctUntilChanged, map, switchMap, first } from 'rxjs/operators';
|
import {
|
||||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
debounceTime,
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
|
distinctUntilChanged,
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
map,
|
||||||
import { SearchService } from 'src/app/services/rest/search.service';
|
switchMap,
|
||||||
import { environment } from 'src/environments/environment';
|
first,
|
||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
|
} from 'rxjs/operators'
|
||||||
import { Meta } from '@angular/platform-browser';
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type';
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||||
|
import { Meta } from '@angular/platform-browser'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-app-frame',
|
selector: 'app-app-frame',
|
||||||
templateUrl: './app-frame.component.html',
|
templateUrl: './app-frame.component.html',
|
||||||
styleUrls: ['./app-frame.component.scss']
|
styleUrls: ['./app-frame.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppFrameComponent {
|
export class AppFrameComponent {
|
||||||
|
constructor(
|
||||||
constructor (
|
|
||||||
public router: Router,
|
public router: Router,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private openDocumentsService: OpenDocumentsService,
|
private openDocumentsService: OpenDocumentsService,
|
||||||
@@ -28,7 +33,7 @@ export class AppFrameComponent {
|
|||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
private list: DocumentListViewService,
|
private list: DocumentListViewService,
|
||||||
private meta: Meta
|
private meta: Meta
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
versionString = `${environment.appTitle} ${environment.version}`
|
versionString = `${environment.appTitle} ${environment.version}`
|
||||||
|
|
||||||
@@ -48,14 +53,14 @@ export class AppFrameComponent {
|
|||||||
text$.pipe(
|
text$.pipe(
|
||||||
debounceTime(200),
|
debounceTime(200),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map(term => {
|
map((term) => {
|
||||||
if (term.lastIndexOf(' ') != -1) {
|
if (term.lastIndexOf(' ') != -1) {
|
||||||
return term.substring(term.lastIndexOf(' ') + 1)
|
return term.substring(term.lastIndexOf(' ') + 1)
|
||||||
} else {
|
} else {
|
||||||
return term
|
return term
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap(term =>
|
switchMap((term) =>
|
||||||
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
|
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -66,49 +71,63 @@ export class AppFrameComponent {
|
|||||||
let lastSpaceIndex = currentSearch.lastIndexOf(' ')
|
let lastSpaceIndex = currentSearch.lastIndexOf(' ')
|
||||||
if (lastSpaceIndex != -1) {
|
if (lastSpaceIndex != -1) {
|
||||||
currentSearch = currentSearch.substring(0, lastSpaceIndex + 1)
|
currentSearch = currentSearch.substring(0, lastSpaceIndex + 1)
|
||||||
currentSearch += event.item + " "
|
currentSearch += event.item + ' '
|
||||||
} else {
|
} else {
|
||||||
currentSearch = event.item + " "
|
currentSearch = event.item + ' '
|
||||||
}
|
}
|
||||||
this.searchField.patchValue(currentSearch)
|
this.searchField.patchValue(currentSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
this.closeMenu()
|
this.closeMenu()
|
||||||
this.list.quickFilter([{rule_type: FILTER_FULLTEXT_QUERY, value: this.searchField.value}])
|
this.list.quickFilter([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
|
value: (this.searchField.value as string).trim(),
|
||||||
|
},
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDocument(d: PaperlessDocument) {
|
closeDocument(d: PaperlessDocument) {
|
||||||
this.openDocumentsService.closeDocument(d).pipe(first()).subscribe(confirmed => {
|
this.openDocumentsService
|
||||||
if (confirmed) {
|
.closeDocument(d)
|
||||||
this.closeMenu()
|
.pipe(first())
|
||||||
let route = this.activatedRoute.snapshot
|
.subscribe((confirmed) => {
|
||||||
while (route.firstChild) {
|
if (confirmed) {
|
||||||
route = route.firstChild
|
this.closeMenu()
|
||||||
|
let route = this.activatedRoute.snapshot
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
route.component == DocumentDetailComponent &&
|
||||||
|
route.params['id'] == d.id
|
||||||
|
) {
|
||||||
|
this.router.navigate([''])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (route.component == DocumentDetailComponent && route.params['id'] == d.id) {
|
})
|
||||||
this.router.navigate([""])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAll() {
|
closeAll() {
|
||||||
// user may need to confirm losing unsaved changes
|
// user may need to confirm losing unsaved changes
|
||||||
this.openDocumentsService.closeAll().pipe(first()).subscribe(confirmed => {
|
this.openDocumentsService
|
||||||
if (confirmed) {
|
.closeAll()
|
||||||
this.closeMenu()
|
.pipe(first())
|
||||||
|
.subscribe((confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.closeMenu()
|
||||||
|
|
||||||
// TODO: is there a better way to do this?
|
// TODO: is there a better way to do this?
|
||||||
let route = this.activatedRoute
|
let route = this.activatedRoute
|
||||||
while (route.firstChild) {
|
while (route.firstChild) {
|
||||||
route = route.firstChild
|
route = route.firstChild
|
||||||
|
}
|
||||||
|
if (route.component === DocumentDetailComponent) {
|
||||||
|
this.router.navigate([''])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (route.component === DocumentDetailComponent) {
|
})
|
||||||
this.router.navigate([""])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get displayName() {
|
get displayName() {
|
||||||
@@ -123,5 +142,4 @@ export class AppFrameComponent {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,9 +8,12 @@
|
|||||||
<p *ngIf="message">{{message}}</p>
|
<p *ngIf="message">{{message}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
|
||||||
|
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span>
|
||||||
|
</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>
|
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||||
|
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ConfirmDialogComponent } from './confirm-dialog.component';
|
|
||||||
|
|
||||||
describe('ConfirmDialogComponent', () => {
|
|
||||||
let component: ConfirmDialogComponent;
|
|
||||||
let fixture: ComponentFixture<ConfirmDialogComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ ConfirmDialogComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ConfirmDialogComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,15 +1,14 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Subject } from 'rxjs';
|
import { interval, Subject, switchMap, take } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-confirm-dialog',
|
selector: 'app-confirm-dialog',
|
||||||
templateUrl: './confirm-dialog.component.html',
|
templateUrl: './confirm-dialog.component.html',
|
||||||
styleUrls: ['./confirm-dialog.component.scss']
|
styleUrls: ['./confirm-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class ConfirmDialogComponent {
|
export class ConfirmDialogComponent {
|
||||||
|
constructor(public activeModal: NgbActiveModal) {}
|
||||||
constructor(public activeModal: NgbActiveModal) { }
|
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
public confirmClicked = new EventEmitter()
|
public confirmClicked = new EventEmitter()
|
||||||
@@ -24,7 +23,7 @@ export class ConfirmDialogComponent {
|
|||||||
message
|
message
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
btnClass = "btn-primary"
|
btnClass = 'btn-primary'
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
btnCaption = $localize`Confirm`
|
btnCaption = $localize`Confirm`
|
||||||
@@ -34,19 +33,28 @@ export class ConfirmDialogComponent {
|
|||||||
|
|
||||||
confirmButtonEnabled = true
|
confirmButtonEnabled = true
|
||||||
seconds = 0
|
seconds = 0
|
||||||
|
secondsTotal = 0
|
||||||
|
|
||||||
confirmSubject: Subject<boolean>
|
confirmSubject: Subject<boolean>
|
||||||
|
|
||||||
delayConfirm(seconds: number) {
|
delayConfirm(seconds: number) {
|
||||||
this.confirmButtonEnabled = false
|
const refreshInterval = 0.15 // s
|
||||||
|
|
||||||
|
this.secondsTotal = seconds
|
||||||
this.seconds = seconds
|
this.seconds = seconds
|
||||||
setTimeout(() => {
|
|
||||||
if (this.seconds <= 1) {
|
interval(refreshInterval * 1000)
|
||||||
this.confirmButtonEnabled = true
|
.pipe(
|
||||||
} else {
|
take(this.secondsTotal / refreshInterval + 2) // need 2 more for animation to complete after 0
|
||||||
this.delayConfirm(seconds - 1)
|
)
|
||||||
}
|
.subscribe((count) => {
|
||||||
}, 1000)
|
this.seconds = Math.max(
|
||||||
|
0,
|
||||||
|
this.secondsTotal - refreshInterval * (count + 1)
|
||||||
|
)
|
||||||
|
this.confirmButtonEnabled =
|
||||||
|
this.secondsTotal - refreshInterval * count < 0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
|
@@ -20,8 +20,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()"
|
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||||
[(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
||||||
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
||||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()"
|
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||||
[(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
||||||
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
||||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { DateDropdownComponent } from './date-dropdown.component';
|
|
||||||
|
|
||||||
describe('DateDropdownComponent', () => {
|
|
||||||
let component: DateDropdownComponent;
|
|
||||||
let fixture: ComponentFixture<DateDropdownComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ DateDropdownComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(DateDropdownComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,10 +1,17 @@
|
|||||||
import { formatDate } from '@angular/common';
|
import { formatDate } from '@angular/common'
|
||||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
|
import {
|
||||||
import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
|
Component,
|
||||||
import { Subject, Subscription } from 'rxjs';
|
EventEmitter,
|
||||||
import { debounceTime } from 'rxjs/operators';
|
Input,
|
||||||
import { SettingsService } from 'src/app/services/settings.service';
|
Output,
|
||||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter';
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Subject, Subscription } from 'rxjs'
|
||||||
|
import { debounceTime } from 'rxjs/operators'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||||
|
|
||||||
export interface DateSelection {
|
export interface DateSelection {
|
||||||
before?: string
|
before?: string
|
||||||
@@ -20,21 +27,18 @@ const LAST_YEAR = 3
|
|||||||
selector: 'app-date-dropdown',
|
selector: 'app-date-dropdown',
|
||||||
templateUrl: './date-dropdown.component.html',
|
templateUrl: './date-dropdown.component.html',
|
||||||
styleUrls: ['./date-dropdown.component.scss'],
|
styleUrls: ['./date-dropdown.component.scss'],
|
||||||
providers: [
|
providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }],
|
||||||
{provide: NgbDateAdapter, useClass: ISODateAdapter},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class DateDropdownComponent implements OnInit, OnDestroy {
|
export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor(settings: SettingsService) {
|
constructor(settings: SettingsService) {
|
||||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||||
}
|
}
|
||||||
|
|
||||||
quickFilters = [
|
quickFilters = [
|
||||||
{id: LAST_7_DAYS, name: $localize`Last 7 days`},
|
{ id: LAST_7_DAYS, name: $localize`Last 7 days` },
|
||||||
{id: LAST_MONTH, name: $localize`Last month`},
|
{ id: LAST_MONTH, name: $localize`Last month` },
|
||||||
{id: LAST_3_MONTHS, name: $localize`Last 3 months`},
|
{ id: LAST_3_MONTHS, name: $localize`Last 3 months` },
|
||||||
{id: LAST_YEAR, name: $localize`Last year`}
|
{ id: LAST_YEAR, name: $localize`Last year` },
|
||||||
]
|
]
|
||||||
|
|
||||||
datePlaceHolder: string
|
datePlaceHolder: string
|
||||||
@@ -62,9 +66,7 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
private sub: Subscription
|
private sub: Subscription
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.sub = this.datesSetDebounce$.pipe(
|
this.sub = this.datesSetDebounce$.pipe(debounceTime(400)).subscribe(() => {
|
||||||
debounceTime(400)
|
|
||||||
).subscribe(() => {
|
|
||||||
this.onChange()
|
this.onChange()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -81,11 +83,11 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
switch (qf) {
|
switch (qf) {
|
||||||
case LAST_7_DAYS:
|
case LAST_7_DAYS:
|
||||||
date.setDate(date.getDate() - 7)
|
date.setDate(date.getDate() - 7)
|
||||||
break;
|
break
|
||||||
|
|
||||||
case LAST_MONTH:
|
case LAST_MONTH:
|
||||||
date.setMonth(date.getMonth() - 1)
|
date.setMonth(date.getMonth() - 1)
|
||||||
break;
|
break
|
||||||
|
|
||||||
case LAST_3_MONTHS:
|
case LAST_3_MONTHS:
|
||||||
date.setMonth(date.getMonth() - 3)
|
date.setMonth(date.getMonth() - 3)
|
||||||
@@ -94,20 +96,22 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
case LAST_YEAR:
|
case LAST_YEAR:
|
||||||
date.setFullYear(date.getFullYear() - 1)
|
date.setFullYear(date.getFullYear() - 1)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
this.dateAfter = formatDate(date, 'yyyy-MM-dd', 'en-us', 'UTC')
|
||||||
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
|
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange() {
|
onChange() {
|
||||||
this.dateAfterChange.emit(this.dateAfter)
|
this.dateAfterChange.emit(this.dateAfter)
|
||||||
this.dateBeforeChange.emit(this.dateBefore)
|
this.dateBeforeChange.emit(this.dateBefore)
|
||||||
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
|
this.datesSet.emit({ after: this.dateAfter, before: this.dateBefore })
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeDebounce() {
|
onChangeDebounce() {
|
||||||
this.datesSetDebounce$.next({after: this.dateAfter, before: this.dateBefore})
|
this.datesSetDebounce$.next({
|
||||||
|
after: this.dateAfter,
|
||||||
|
before: this.dateBefore,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearBefore() {
|
clearBefore() {
|
||||||
@@ -120,4 +124,10 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
|||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prevent chars other than numbers and separators
|
||||||
|
onKeyPress(event: KeyboardEvent) {
|
||||||
|
if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@@ -1,19 +1,22 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core'
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-correspondent-edit-dialog',
|
selector: 'app-correspondent-edit-dialog',
|
||||||
templateUrl: './correspondent-edit-dialog.component.html',
|
templateUrl: './correspondent-edit-dialog.component.html',
|
||||||
styleUrls: ['./correspondent-edit-dialog.component.scss']
|
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||||
|
constructor(
|
||||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
|
service: CorrespondentService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
toastService: ToastService
|
||||||
|
) {
|
||||||
super(service, activeModal, toastService)
|
super(service, activeModal, toastService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,9 +32,8 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
|
|||||||
return new FormGroup({
|
return new FormGroup({
|
||||||
name: new FormControl(''),
|
name: new FormControl(''),
|
||||||
matching_algorithm: new FormControl(1),
|
matching_algorithm: new FormControl(1),
|
||||||
match: new FormControl(""),
|
match: new FormControl(''),
|
||||||
is_insensitive: new FormControl(true)
|
is_insensitive: new FormControl(true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@@ -1,19 +1,22 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core'
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-type-edit-dialog',
|
selector: 'app-document-type-edit-dialog',
|
||||||
templateUrl: './document-type-edit-dialog.component.html',
|
templateUrl: './document-type-edit-dialog.component.html',
|
||||||
styleUrls: ['./document-type-edit-dialog.component.scss']
|
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||||
|
constructor(
|
||||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
service: DocumentTypeService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
toastService: ToastService
|
||||||
|
) {
|
||||||
super(service, activeModal, toastService)
|
super(service, activeModal, toastService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,9 +32,8 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
|
|||||||
return new FormGroup({
|
return new FormGroup({
|
||||||
name: new FormControl(''),
|
name: new FormControl(''),
|
||||||
matching_algorithm: new FormControl(1),
|
matching_algorithm: new FormControl(1),
|
||||||
match: new FormControl(""),
|
match: new FormControl(''),
|
||||||
is_insensitive: new FormControl(true)
|
is_insensitive: new FormControl(true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { EditDialogComponent } from './edit-dialog.component';
|
|
||||||
|
|
||||||
describe('EditDialogComponent', () => {
|
|
||||||
let component: EditDialogComponent;
|
|
||||||
let fixture: ComponentFixture<EditDialogComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ EditDialogComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(EditDialogComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,20 +1,22 @@
|
|||||||
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators'
|
||||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
|
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
|
export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
private service: AbstractPaperlessService<T>,
|
||||||
private activeModal: NgbActiveModal,
|
private activeModal: NgbActiveModal,
|
||||||
private toastService: ToastService) { }
|
private toastService: ToastService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dialogMode: string = 'create'
|
dialogMode: string = 'create'
|
||||||
@@ -43,7 +45,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
|
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.closeEnabled = true
|
this.closeEnabled = true
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
@@ -65,7 +67,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
case 'edit':
|
case 'edit':
|
||||||
return this.getEditTitle()
|
return this.getEditTitle()
|
||||||
default:
|
default:
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,25 +80,31 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
|
var newObject = Object.assign(
|
||||||
|
Object.assign({}, this.object),
|
||||||
|
this.objectForm.value
|
||||||
|
)
|
||||||
var serverResponse: Observable<T>
|
var serverResponse: Observable<T>
|
||||||
switch (this.dialogMode) {
|
switch (this.dialogMode) {
|
||||||
case 'create':
|
case 'create':
|
||||||
serverResponse = this.service.create(newObject)
|
serverResponse = this.service.create(newObject)
|
||||||
break;
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
serverResponse = this.service.update(newObject)
|
serverResponse = this.service.update(newObject)
|
||||||
default:
|
default:
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
this.networkActive = true
|
this.networkActive = true
|
||||||
serverResponse.subscribe(result => {
|
serverResponse.subscribe(
|
||||||
this.activeModal.close()
|
(result) => {
|
||||||
this.success.emit(result)
|
this.activeModal.close()
|
||||||
}, error => {
|
this.success.emit(result)
|
||||||
this.error = error.error
|
},
|
||||||
this.networkActive = false
|
(error) => {
|
||||||
})
|
this.error = error.error
|
||||||
|
this.networkActive = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||||
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { randomColor } from 'src/app/utils/color'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tag-edit-dialog',
|
||||||
|
templateUrl: './tag-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||||
|
constructor(
|
||||||
|
service: TagService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
toastService: ToastService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, toastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
color: new FormControl(randomColor()),
|
||||||
|
is_inbox_tag: new FormControl(false),
|
||||||
|
matching_algorithm: new FormControl(1),
|
||||||
|
match: new FormControl(''),
|
||||||
|
is_insensitive: new FormControl(true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -6,7 +6,7 @@
|
|||||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||||
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
||||||
<div *ngIf="multiple" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light text-light rounded-pill">
|
<div *ngIf="multiple" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light text-light rounded-pill">
|
||||||
{{selectionModel.selectionSize()}}<span class="visually-hidden">selected</span>
|
{{selectionModel.totalCount}}<span class="visually-hidden">selected</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!multiple" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
<div *ngIf="!multiple" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
||||||
<span class="visually-hidden">selected</span>
|
<span class="visually-hidden">selected</span>
|
||||||
@@ -18,10 +18,10 @@
|
|||||||
<div *ngIf="!editing && multiple" class="list-group-item d-flex">
|
<div *ngIf="!editing && multiple" class="list-group-item d-flex">
|
||||||
<div class="btn-group btn-group-xs btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="selectionModel.logicalOperator" (change)="selectionModel.toggleOperator()" [disabled]="!operatorToggleEnabled">
|
<div class="btn-group btn-group-xs btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="selectionModel.logicalOperator" (change)="selectionModel.toggleOperator()" [disabled]="!operatorToggleEnabled">
|
||||||
<label ngbButtonLabel class="btn btn-outline-primary">
|
<label ngbButtonLabel class="btn btn-outline-primary">
|
||||||
<input ngbButton type="radio" class="btn-check" name="logicalOperator" value="and"> All
|
<input ngbButton type="radio" class="btn-check" name="logicalOperator" value="and" i18n> All
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn btn-outline-primary">
|
<label ngbButtonLabel class="btn btn-outline-primary">
|
||||||
<input ngbButton type="radio" class="btn-check" name="logicalOperator" value="or"> Any
|
<input ngbButton type="radio" class="btn-check" name="logicalOperator" value="or" i18n> Any
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -42,7 +42,7 @@
|
|||||||
filter: brightness(0.5);
|
filter: brightness(0.5);
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: lighten($primary, 30%);
|
background-color: var(--ngx-primary-lighten-30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { FilterableDropodownComponent } from './filterable-dropdown.component';
|
|
||||||
|
|
||||||
describe('FilterableDropodownComponent', () => {
|
|
||||||
let component: FilterableDropodownComponent;
|
|
||||||
let fixture: ComponentFixture<FilterableDropodownComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ FilterableDropodownComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(FilterableDropodownComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,17 +1,23 @@
|
|||||||
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
|
import {
|
||||||
import { FilterPipe } from 'src/app/pipes/filter.pipe';
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
|
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
import { MatchingModel } from 'src/app/data/matching-model';
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs'
|
||||||
|
|
||||||
export interface ChangedItems {
|
export interface ChangedItems {
|
||||||
itemsToAdd: MatchingModel[],
|
itemsToAdd: MatchingModel[]
|
||||||
itemsToRemove: MatchingModel[]
|
itemsToRemove: MatchingModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FilterableDropdownSelectionModel {
|
export class FilterableDropdownSelectionModel {
|
||||||
|
|
||||||
changed = new Subject<FilterableDropdownSelectionModel>()
|
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
multiple = false
|
multiple = false
|
||||||
@@ -22,14 +28,20 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
get itemsSorted(): MatchingModel[] {
|
get itemsSorted(): MatchingModel[] {
|
||||||
// TODO: this is getting called very often
|
// TODO: this is getting called very often
|
||||||
return this.items.sort((a,b) => {
|
return this.items.sort((a, b) => {
|
||||||
if (a.id == null && b.id != null) {
|
if (a.id == null && b.id != null) {
|
||||||
return -1
|
return -1
|
||||||
} else if (a.id != null && b.id == null) {
|
} else if (a.id != null && b.id == null) {
|
||||||
return 1
|
return 1
|
||||||
} else if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
|
} else if (
|
||||||
|
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||||
|
this.getNonTemporary(b.id) != ToggleableItemState.NotSelected
|
||||||
|
) {
|
||||||
return 1
|
return 1
|
||||||
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
|
} else if (
|
||||||
|
this.getNonTemporary(a.id) != ToggleableItemState.NotSelected &&
|
||||||
|
this.getNonTemporary(b.id) == ToggleableItemState.NotSelected
|
||||||
|
) {
|
||||||
return -1
|
return -1
|
||||||
} else {
|
} else {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
@@ -42,11 +54,17 @@ export class FilterableDropdownSelectionModel {
|
|||||||
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
getSelectedItems() {
|
getSelectedItems() {
|
||||||
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
|
return this.items.filter(
|
||||||
|
(i) =>
|
||||||
|
this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getExcludedItems() {
|
getExcludedItems() {
|
||||||
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded)
|
return this.items.filter(
|
||||||
|
(i) =>
|
||||||
|
this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
||||||
@@ -62,9 +80,16 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
toggle(id: number, fireEvent = true) {
|
toggle(id: number, fireEvent = true) {
|
||||||
let state = this.temporarySelectionStates.get(id)
|
let state = this.temporarySelectionStates.get(id)
|
||||||
if (state == null || (state != ToggleableItemState.Selected && state != ToggleableItemState.Excluded)) {
|
if (
|
||||||
|
state == null ||
|
||||||
|
(state != ToggleableItemState.Selected &&
|
||||||
|
state != ToggleableItemState.Excluded)
|
||||||
|
) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||||
} else if (state == ToggleableItemState.Selected || state == ToggleableItemState.Excluded) {
|
} else if (
|
||||||
|
state == ToggleableItemState.Selected ||
|
||||||
|
state == ToggleableItemState.Excluded
|
||||||
|
) {
|
||||||
this.temporarySelectionStates.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +116,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exclude(id: number, fireEvent:boolean = true) {
|
exclude(id: number, fireEvent: boolean = true) {
|
||||||
let state = this.temporarySelectionStates.get(id)
|
let state = this.temporarySelectionStates.get(id)
|
||||||
if (state == null || state != ToggleableItemState.Excluded) {
|
if (state == null || state != ToggleableItemState.Excluded) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||||
@@ -130,13 +155,19 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(id: number) {
|
get(id: number) {
|
||||||
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
return (
|
||||||
|
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectionSize() {
|
selectionSize() {
|
||||||
return this.getSelectedItems().length
|
return this.getSelectedItems().length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get totalCount() {
|
||||||
|
return this.getSelectedItems().length + this.getExcludedItems().length
|
||||||
|
}
|
||||||
|
|
||||||
clear(fireEvent = true) {
|
clear(fireEvent = true) {
|
||||||
this.temporarySelectionStates.clear()
|
this.temporarySelectionStates.clear()
|
||||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
||||||
@@ -146,9 +177,19 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isDirty() {
|
isDirty() {
|
||||||
if (!Array.from(this.temporarySelectionStates.keys()).every(id => this.temporarySelectionStates.get(id) == this.selectionStates.get(id))) {
|
if (
|
||||||
|
!Array.from(this.temporarySelectionStates.keys()).every(
|
||||||
|
(id) =>
|
||||||
|
this.temporarySelectionStates.get(id) == this.selectionStates.get(id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
} else if (!Array.from(this.selectionStates.keys()).every(id => this.selectionStates.get(id) == this.temporarySelectionStates.get(id))) {
|
} else if (
|
||||||
|
!Array.from(this.selectionStates.keys()).every(
|
||||||
|
(id) =>
|
||||||
|
this.selectionStates.get(id) == this.temporarySelectionStates.get(id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
||||||
return true
|
return true
|
||||||
@@ -158,7 +199,10 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isNoneSelected() {
|
isNoneSelected() {
|
||||||
return this.selectionSize() == 1 && this.get(null) == ToggleableItemState.Selected
|
return (
|
||||||
|
this.selectionSize() == 1 &&
|
||||||
|
this.get(null) == ToggleableItemState.Selected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(map) {
|
init(map) {
|
||||||
@@ -183,8 +227,17 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
diff(): ChangedItems {
|
diff(): ChangedItems {
|
||||||
return {
|
return {
|
||||||
itemsToAdd: this.items.filter(item => this.temporarySelectionStates.get(item.id) == ToggleableItemState.Selected && this.selectionStates.get(item.id) != ToggleableItemState.Selected),
|
itemsToAdd: this.items.filter(
|
||||||
itemsToRemove: this.items.filter(item => !this.temporarySelectionStates.has(item.id) && this.selectionStates.has(item.id)),
|
(item) =>
|
||||||
|
this.temporarySelectionStates.get(item.id) ==
|
||||||
|
ToggleableItemState.Selected &&
|
||||||
|
this.selectionStates.get(item.id) != ToggleableItemState.Selected
|
||||||
|
),
|
||||||
|
itemsToRemove: this.items.filter(
|
||||||
|
(item) =>
|
||||||
|
!this.temporarySelectionStates.has(item.id) &&
|
||||||
|
this.selectionStates.has(item.id)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +245,9 @@ export class FilterableDropdownSelectionModel {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-filterable-dropdown',
|
selector: 'app-filterable-dropdown',
|
||||||
templateUrl: './filterable-dropdown.component.html',
|
templateUrl: './filterable-dropdown.component.html',
|
||||||
styleUrls: ['./filterable-dropdown.component.scss']
|
styleUrls: ['./filterable-dropdown.component.scss'],
|
||||||
})
|
})
|
||||||
export class FilterableDropdownComponent {
|
export class FilterableDropdownComponent {
|
||||||
|
|
||||||
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
||||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||||
|
|
||||||
@@ -207,7 +259,7 @@ export class FilterableDropdownComponent {
|
|||||||
this._selectionModel.items = Array.from(items)
|
this._selectionModel.items = Array.from(items)
|
||||||
this._selectionModel.items.unshift({
|
this._selectionModel.items.unshift({
|
||||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||||
id: null
|
id: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +277,7 @@ export class FilterableDropdownComponent {
|
|||||||
model.items = this.selectionModel.items
|
model.items = this.selectionModel.items
|
||||||
model.multiple = this.selectionModel.multiple
|
model.multiple = this.selectionModel.multiple
|
||||||
}
|
}
|
||||||
model.changed.subscribe(updatedModel => {
|
model.changed.subscribe((updatedModel) => {
|
||||||
this.selectionModelChange.next(updatedModel)
|
this.selectionModelChange.next(updatedModel)
|
||||||
})
|
})
|
||||||
this._selectionModel = model
|
this._selectionModel = model
|
||||||
@@ -251,7 +303,7 @@ export class FilterableDropdownComponent {
|
|||||||
title: string
|
title: string
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
filterPlaceholder: string = ""
|
filterPlaceholder: string = ''
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
icon: string
|
icon: string
|
||||||
@@ -272,14 +324,17 @@ export class FilterableDropdownComponent {
|
|||||||
open = new EventEmitter()
|
open = new EventEmitter()
|
||||||
|
|
||||||
get operatorToggleEnabled(): boolean {
|
get operatorToggleEnabled(): boolean {
|
||||||
return this.selectionModel.selectionSize() > 1 && this.selectionModel.getExcludedItems().length == 0
|
return (
|
||||||
|
this.selectionModel.selectionSize() > 1 &&
|
||||||
|
this.selectionModel.getExcludedItems().length == 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
modelIsDirty: boolean = false
|
modelIsDirty: boolean = false
|
||||||
|
|
||||||
constructor(private filterPipe: FilterPipe) {
|
constructor(private filterPipe: FilterPipe) {
|
||||||
this.selectionModel = new FilterableDropdownSelectionModel()
|
this.selectionModel = new FilterableDropdownSelectionModel()
|
||||||
this.selectionModelChange.subscribe(updatedModel => {
|
this.selectionModelChange.subscribe((updatedModel) => {
|
||||||
this.modelIsDirty = updatedModel.isDirty()
|
this.modelIsDirty = updatedModel.isDirty()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -296,12 +351,12 @@ export class FilterableDropdownComponent {
|
|||||||
dropdownOpenChange(open: boolean): void {
|
dropdownOpenChange(open: boolean): void {
|
||||||
if (open) {
|
if (open) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.listFilterTextInput.nativeElement.focus();
|
this.listFilterTextInput.nativeElement.focus()
|
||||||
}, 0)
|
}, 0)
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
this.selectionModel.reset()
|
this.selectionModel.reset()
|
||||||
}
|
}
|
||||||
this.open.next()
|
this.open.next(this)
|
||||||
} else {
|
} else {
|
||||||
this.filterText = ''
|
this.filterText = ''
|
||||||
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { ToggleableDropdownButtonComponent } from './toggleable-dropdown-button.component';
|
|
||||||
|
|
||||||
describe('ToggleableDropdownButtonComponent', () => {
|
|
||||||
let component: ToggleableDropdownButtonComponent;
|
|
||||||
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ ToggleableDropdownButtonComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,20 +1,19 @@
|
|||||||
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
|
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'
|
||||||
import { MatchingModel } from 'src/app/data/matching-model';
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
|
|
||||||
export enum ToggleableItemState {
|
export enum ToggleableItemState {
|
||||||
NotSelected = 0,
|
NotSelected = 0,
|
||||||
Selected = 1,
|
Selected = 1,
|
||||||
PartiallySelected = 2,
|
PartiallySelected = 2,
|
||||||
Excluded = 3
|
Excluded = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-toggleable-dropdown-button',
|
selector: 'app-toggleable-dropdown-button',
|
||||||
templateUrl: './toggleable-dropdown-button.component.html',
|
templateUrl: './toggleable-dropdown-button.component.html',
|
||||||
styleUrls: ['./toggleable-dropdown-button.component.scss']
|
styleUrls: ['./toggleable-dropdown-button.component.scss'],
|
||||||
})
|
})
|
||||||
export class ToggleableDropdownButtonComponent {
|
export class ToggleableDropdownButtonComponent {
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
item: MatchingModel
|
item: MatchingModel
|
||||||
|
|
||||||
|
@@ -1,30 +1,29 @@
|
|||||||
import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ControlValueAccessor } from '@angular/forms';
|
import { ControlValueAccessor } from '@angular/forms'
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
||||||
|
@ViewChild('inputField')
|
||||||
@ViewChild("inputField")
|
|
||||||
inputField: ElementRef
|
inputField: ElementRef
|
||||||
|
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
onChange = (newValue: T) => {};
|
onChange = (newValue: T) => {}
|
||||||
|
|
||||||
onTouched = () => {};
|
onTouched = () => {}
|
||||||
|
|
||||||
writeValue(newValue: any): void {
|
writeValue(newValue: any): void {
|
||||||
this.value = newValue
|
this.value = newValue
|
||||||
}
|
}
|
||||||
registerOnChange(fn: any): void {
|
registerOnChange(fn: any): void {
|
||||||
this.onChange = fn;
|
this.onChange = fn
|
||||||
}
|
}
|
||||||
registerOnTouched(fn: any): void {
|
registerOnTouched(fn: any): void {
|
||||||
this.onTouched = fn;
|
this.onTouched = fn
|
||||||
}
|
}
|
||||||
setDisabledState?(isDisabled: boolean): void {
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
this.disabled = isDisabled;
|
this.disabled = isDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
@@ -37,7 +36,7 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
|||||||
title: string
|
title: string
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
disabled = false;
|
disabled = false
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
error: string
|
error: string
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user