mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-30 18:27:45 -05:00
Compare commits
403 Commits
v1.12.1
...
v1.14.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d17e24c6e | ||
![]() |
de155a753d | ||
![]() |
1b4020b3d7 | ||
![]() |
b948750d55 | ||
![]() |
ce41ac9158 | ||
![]() |
5869467db3 | ||
![]() |
80472af53c | ||
![]() |
eaa6039082 | ||
![]() |
03b84e7c43 | ||
![]() |
0ee8f5c498 | ||
![]() |
41d15e8731 | ||
![]() |
bef081d353 | ||
![]() |
a79045c064 | ||
![]() |
81341df635 | ||
![]() |
3c2bbf244d | ||
![]() |
fa60251c18 | ||
![]() |
5bd06494d5 | ||
![]() |
2fd217ef1f | ||
![]() |
d7068ca42b | ||
![]() |
f7b19cddbf | ||
![]() |
62e756a11e | ||
![]() |
c992661a17 | ||
![]() |
dde3205425 | ||
![]() |
15a264de3d | ||
![]() |
66929a9088 | ||
![]() |
45c9518a6d | ||
![]() |
95d32dc0da | ||
![]() |
d5ab1119d3 | ||
![]() |
d9110f4ef7 | ||
![]() |
5468394ef2 | ||
![]() |
7ad21e0e45 | ||
![]() |
5012c0c97c | ||
![]() |
698208fcd5 | ||
![]() |
e0d5fd9290 | ||
![]() |
4bd1974911 | ||
![]() |
d246e4090a | ||
![]() |
ff172f5ea1 | ||
![]() |
09b1413748 | ||
![]() |
14b997fe2c | ||
![]() |
908b412a9a | ||
![]() |
cbd80615be | ||
![]() |
a9707f0ab0 | ||
![]() |
4637e33326 | ||
![]() |
4a5f21dd87 | ||
![]() |
0778c2808b | ||
![]() |
567a1bb770 | ||
![]() |
743ee886be | ||
![]() |
20c6abae63 | ||
![]() |
ae0c585918 | ||
![]() |
4cfc416cdc | ||
![]() |
9902c4745d | ||
![]() |
e373ca7bdc | ||
![]() |
6a34a35585 | ||
![]() |
ee935a2988 | ||
![]() |
0a977a9d0a | ||
![]() |
4d26a3d2c6 | ||
![]() |
276d11e4e8 | ||
![]() |
e89c0f15dd | ||
![]() |
2bdf0aae14 | ||
![]() |
2bc7f0b8e0 | ||
![]() |
bf8ae22f3f | ||
![]() |
a5c6dab7c3 | ||
![]() |
f3eedec402 | ||
![]() |
741152dd50 | ||
![]() |
89c639f850 | ||
![]() |
727fb38baf | ||
![]() |
e19dd2d527 | ||
![]() |
9aa41b3524 | ||
![]() |
3911740360 | ||
![]() |
f161722b34 | ||
![]() |
adb956467b | ||
![]() |
00e17f4d69 | ||
![]() |
dbe49b24df | ||
![]() |
7e75193f4a | ||
![]() |
96e8cfb765 | ||
![]() |
d47ca6109a | ||
![]() |
8ac7d56fc5 | ||
![]() |
bfaede26c4 | ||
![]() |
477fd360f8 | ||
![]() |
5b50870f21 | ||
![]() |
8161316c01 | ||
![]() |
3cb26722f1 | ||
![]() |
7912f4a22a | ||
![]() |
851b23fb09 | ||
![]() |
849a108520 | ||
![]() |
97ff2e126c | ||
![]() |
ef6c4e6789 | ||
![]() |
3e467c517d | ||
![]() |
70ac696f17 | ||
![]() |
39c80ded58 | ||
![]() |
98782ca69d | ||
![]() |
c332eea354 | ||
![]() |
41a3f039b5 | ||
![]() |
2d3cf43bc5 | ||
![]() |
7c90702ff7 | ||
![]() |
952cf11b9e | ||
![]() |
4383550d98 | ||
![]() |
fcba2cca77 | ||
![]() |
2042b85056 | ||
![]() |
283a2ab648 | ||
![]() |
5e7b93d153 | ||
![]() |
c4ac35164b | ||
![]() |
22a13981f3 | ||
![]() |
29251b6e38 | ||
![]() |
b382f1412a | ||
![]() |
320537a054 | ||
![]() |
2fe7f8be46 | ||
![]() |
f100198a8a | ||
![]() |
b470fc0140 | ||
![]() |
db02d5eff0 | ||
![]() |
9564a9c28d | ||
![]() |
55295922c8 | ||
![]() |
c5b701f99d | ||
![]() |
3c606efc46 | ||
![]() |
cbab1a51f1 | ||
![]() |
41bcfcaffe | ||
![]() |
7cb14374cf | ||
![]() |
19b9fd0578 | ||
![]() |
d668c475de | ||
![]() |
248f3f2181 | ||
![]() |
bcd10f63ea | ||
![]() |
db9733f0d5 | ||
![]() |
e6aa213aa1 | ||
![]() |
f0fa726e71 | ||
![]() |
ae46ef7add | ||
![]() |
c87ca25f22 | ||
![]() |
ef627d53e5 | ||
![]() |
d0fcf3607d | ||
![]() |
3dbb7e5781 | ||
![]() |
9597358cb0 | ||
![]() |
c5a21a3b0e | ||
![]() |
f56ccec77f | ||
![]() |
489340a338 | ||
![]() |
e9e3d75383 | ||
![]() |
74a1e5ed86 | ||
![]() |
3d961a3dbb | ||
![]() |
604d56d0b8 | ||
![]() |
a82e259c1d | ||
![]() |
34ef99a8ab | ||
![]() |
fa389d19d0 | ||
![]() |
ebe70d996c | ||
![]() |
e83e13fd57 | ||
![]() |
02362ae5e1 | ||
![]() |
e0f324f61c | ||
![]() |
c422a081bf | ||
![]() |
72efb24b73 | ||
![]() |
ee7653a8cd | ||
![]() |
020abaf7d6 | ||
![]() |
6e2f6350e6 | ||
![]() |
6b939f7567 | ||
![]() |
c958a7c593 | ||
![]() |
8709ea4df0 | ||
![]() |
16d3041d7a | ||
![]() |
64b2037eda | ||
![]() |
4133001c73 | ||
![]() |
d5f46eedab | ||
![]() |
7a2a3e048e | ||
![]() |
20891a370b | ||
![]() |
ca412e0184 | ||
![]() |
8a89f5ae27 | ||
![]() |
dbea2acc8f | ||
![]() |
65167625c4 | ||
![]() |
77b23d3acb | ||
![]() |
5d8aa27831 | ||
![]() |
b15eda9466 | ||
![]() |
5eb4b975ae | ||
![]() |
3ce1e01d96 | ||
![]() |
afcefe3d04 | ||
![]() |
4726fe8b6f | ||
![]() |
201a4a7ef9 | ||
![]() |
93a6391f96 | ||
![]() |
782db3f324 | ||
![]() |
7610a0459e | ||
![]() |
927616decb | ||
![]() |
8b2b7bbe6d | ||
![]() |
ca30dbc832 | ||
![]() |
3c3c847db5 | ||
![]() |
b7a2601724 | ||
![]() |
1189df1fe6 | ||
![]() |
a37177703c | ||
![]() |
8df1324afd | ||
![]() |
a6e2708605 | ||
![]() |
0df91c31f1 | ||
![]() |
1718cf6504 | ||
![]() |
bec8d00232 | ||
![]() |
1471dd72a6 | ||
![]() |
588a786d73 | ||
![]() |
7e1c1da424 | ||
![]() |
94ebe3b61c | ||
![]() |
75c5ccccec | ||
![]() |
eb4c8e1b1e | ||
![]() |
73b1b942a9 | ||
![]() |
8c5ef111d8 | ||
![]() |
b6266ad18f | ||
![]() |
2635c3a1a0 | ||
![]() |
13ece25de0 | ||
![]() |
c69ece1d0e | ||
![]() |
07ec6ff7ab | ||
![]() |
66e23bd356 | ||
![]() |
abc58000b4 | ||
![]() |
5e3ef94697 | ||
![]() |
2daee375d0 | ||
![]() |
857944aabe | ||
![]() |
72f58d54a3 | ||
![]() |
668b068bb5 | ||
![]() |
9893ae9880 | ||
![]() |
9cdf2f046f | ||
![]() |
f7f841ce6d | ||
![]() |
e8a52d48cf | ||
![]() |
21eb253c57 | ||
![]() |
754286cb9a | ||
![]() |
08f5d9a92f | ||
![]() |
a1a61000ab | ||
![]() |
dd91d4264a | ||
![]() |
9d87ac5244 | ||
![]() |
3559e27cdd | ||
![]() |
4b9c79fa07 | ||
![]() |
ee197bf89f | ||
![]() |
31dac60d04 | ||
![]() |
75a7dd38a1 | ||
![]() |
6c658a676e | ||
![]() |
38de2a7767 | ||
![]() |
d7cb7c78af | ||
![]() |
ee272da95f | ||
![]() |
8098ac6b15 | ||
![]() |
c08f0054da | ||
![]() |
29f8cda104 | ||
![]() |
0d1a8d6d2f | ||
![]() |
edfd3bbe91 | ||
![]() |
148f394679 | ||
![]() |
b91ec5a520 | ||
![]() |
f57873fb1b | ||
![]() |
32754defef | ||
![]() |
0d777343fb | ||
![]() |
3b0fa4f707 | ||
![]() |
8b3d01c49b | ||
![]() |
1d5eb983ea | ||
![]() |
add647afe6 | ||
![]() |
3e777f2a5b | ||
![]() |
7bfb11a711 | ||
![]() |
808cf93a19 | ||
![]() |
37ddc3b8f7 | ||
![]() |
bc91b830ed | ||
![]() |
ced248ad49 | ||
![]() |
d73fbb1643 | ||
![]() |
40db244d4a | ||
![]() |
8181535f40 | ||
![]() |
2d9fa58157 | ||
![]() |
7af0b47ba9 | ||
![]() |
b9f0418038 | ||
![]() |
74b729bf5a | ||
![]() |
66333caebc | ||
![]() |
405c0922f8 | ||
![]() |
bdf9b2453f | ||
![]() |
8ef5f0e93c | ||
![]() |
597bb98cb9 | ||
![]() |
cc971a4569 | ||
![]() |
8955d25a00 | ||
![]() |
bdcba570cb | ||
![]() |
0e83c94832 | ||
![]() |
d2a6f79612 | ||
![]() |
8154c7b53a | ||
![]() |
ac611acaa1 | ||
![]() |
faecd59432 | ||
![]() |
0f536a9b9a | ||
![]() |
a203b006e7 | ||
![]() |
5978650ec6 | ||
![]() |
8fea4c00ad | ||
![]() |
9b3ec22beb | ||
![]() |
54e1c17539 | ||
![]() |
06e2500443 | ||
![]() |
f43c3e0dd6 | ||
![]() |
73b2c366df | ||
![]() |
1547a698cb | ||
![]() |
87b1f5adec | ||
![]() |
8a281452b8 | ||
![]() |
dd38b84116 | ||
![]() |
658d372cd2 | ||
![]() |
61f7e73961 | ||
![]() |
31bcb613ad | ||
![]() |
b85d39b189 | ||
![]() |
dc7bef5d48 | ||
![]() |
dae4550bc3 | ||
![]() |
dace08d4e3 | ||
![]() |
c42a9b8b8f | ||
![]() |
94db39e055 | ||
![]() |
9ee324915f | ||
![]() |
bb211ee779 | ||
![]() |
00c3d8c523 | ||
![]() |
4f0d2cd612 | ||
![]() |
f4304a120b | ||
![]() |
b8c3e564c6 | ||
![]() |
0a47fba9ae | ||
![]() |
9aea8a7d7c | ||
![]() |
7b9c0d65b9 | ||
![]() |
7dd9a4e089 | ||
![]() |
d15773f282 | ||
![]() |
9784ea4a60 | ||
![]() |
4fce5aba63 | ||
![]() |
2ab77fbaf7 | ||
![]() |
94ad290e14 | ||
![]() |
118e3703dc | ||
![]() |
d2b290f789 | ||
![]() |
583f05af2d | ||
![]() |
1b2cb13a21 | ||
![]() |
4dc0c7bbe2 | ||
![]() |
44212d492d | ||
![]() |
3ccb83e49c | ||
![]() |
215691ac1a | ||
![]() |
a884647a7c | ||
![]() |
590d129cd3 | ||
![]() |
8fcb7efbd2 | ||
![]() |
f1204d2749 | ||
![]() |
b07b8d65a6 | ||
![]() |
dadd7472fd | ||
![]() |
2801b60b0e | ||
![]() |
e625ac21c3 | ||
![]() |
7ace9eb325 | ||
![]() |
02465672f9 | ||
![]() |
9b1b620a9c | ||
![]() |
6ea6c79575 | ||
![]() |
c430b9f8cf | ||
![]() |
f78e93a364 | ||
![]() |
6d3feaebfd | ||
![]() |
781929e9a8 | ||
![]() |
5fc7c15039 | ||
![]() |
44f860d9b0 | ||
![]() |
0cfa5211e9 | ||
![]() |
d689a707a4 | ||
![]() |
55e1745889 | ||
![]() |
43ec058593 | ||
![]() |
a4d96061de | ||
![]() |
f1eecd146d | ||
![]() |
1663450c1f | ||
![]() |
d840308392 | ||
![]() |
a08467342c | ||
![]() |
d71d388c08 | ||
![]() |
e2093436ac | ||
![]() |
b7e2013589 | ||
![]() |
f31cee75f3 | ||
![]() |
ec27f3c053 | ||
![]() |
737f00df3a | ||
![]() |
e6804dad2f | ||
![]() |
1875e9852e | ||
![]() |
f021e7fcc3 | ||
![]() |
4cf9ed9d26 | ||
![]() |
f8b77d7ef7 | ||
![]() |
31850c3351 | ||
![]() |
446842ecfc | ||
![]() |
8159b7574c | ||
![]() |
7050f29cff | ||
![]() |
2f32565476 | ||
![]() |
ceeb2da3fe | ||
![]() |
6dc5c1de32 | ||
![]() |
a5ab6f2558 | ||
![]() |
f846c2934c | ||
![]() |
1adb5e724d | ||
![]() |
8fad13b500 | ||
![]() |
6b2ee0e301 | ||
![]() |
7a241950d4 | ||
![]() |
c1a1f6d74e | ||
![]() |
b99422da12 | ||
![]() |
2ec695fba7 | ||
![]() |
109c07b23b | ||
![]() |
bf34c955ff | ||
![]() |
6ece5240a5 | ||
![]() |
211fbf0cf6 | ||
![]() |
f2d635671d | ||
![]() |
8b204cac99 | ||
![]() |
d15c701510 | ||
![]() |
c73688d167 | ||
![]() |
32da039d5f | ||
![]() |
79da613cb6 | ||
![]() |
2973e4672a | ||
![]() |
18e0012a59 | ||
![]() |
2554ced198 | ||
![]() |
fad13c148e | ||
![]() |
dbaa606a9f | ||
![]() |
c0bccc6a95 | ||
![]() |
b1a4eec7be | ||
![]() |
bb8a0d26e2 | ||
![]() |
629a5dd61e | ||
![]() |
b21970fd53 | ||
![]() |
4279ba13e9 | ||
![]() |
28d70438ec | ||
![]() |
ca6454f9fd | ||
![]() |
0ffc9955b2 | ||
![]() |
cf53d0866a | ||
![]() |
3f19c0ed03 | ||
![]() |
355efadf87 | ||
![]() |
927a9781ad | ||
![]() |
70eb22df42 | ||
![]() |
f461485aa0 | ||
![]() |
b6f1ced455 | ||
![]() |
10f36870e6 | ||
![]() |
fdaf9e9b46 | ||
![]() |
57b709824f | ||
![]() |
c7b46ac861 | ||
![]() |
bf28a512c6 | ||
![]() |
4333bd58cf | ||
![]() |
96a29883cd | ||
![]() |
59e359ff98 | ||
![]() |
4603813896 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"qpdf": {
|
||||
"version": "11.2.0"
|
||||
"version": "11.3.0"
|
||||
},
|
||||
"jbig2enc": {
|
||||
"version": "0.29",
|
||||
|
19
.codecov.yml
Normal file
19
.codecov.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
# https://docs.codecov.com/docs/pull-request-comments
|
||||
# codecov will only comment if coverage changes
|
||||
comment:
|
||||
require_changes: true
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# https://docs.codecov.com/docs/commit-status#threshold
|
||||
threshold: 1%
|
||||
# https://docs.codecov.com/docs/commit-status#only_pulls
|
||||
only_pulls: true
|
||||
patch:
|
||||
default:
|
||||
# For the changed lines only, target 75% covered, but
|
||||
# allow as low as 50%
|
||||
target: 75%
|
||||
threshold: 25%
|
||||
only_pulls: true
|
4
.github/release-drafter.yml
vendored
4
.github/release-drafter.yml
vendored
@@ -40,7 +40,7 @@ categories:
|
||||
labels:
|
||||
- 'frontend'
|
||||
- 'backend'
|
||||
collapse-after: 0
|
||||
collapse-after: 1
|
||||
include-labels:
|
||||
- 'enhancement'
|
||||
- 'bug'
|
||||
@@ -54,6 +54,8 @@ include-labels:
|
||||
- 'ci-cd'
|
||||
- 'breaking-change'
|
||||
- 'notable'
|
||||
exclude-labels:
|
||||
- 'skip-changelog'
|
||||
category-template: '### $TITLE'
|
||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||
change-title-escapes: '\<*_&#@'
|
||||
|
132
.github/scripts/cleanup-tags.py
vendored
132
.github/scripts/cleanup-tags.py
vendored
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -7,6 +6,7 @@ import subprocess
|
||||
from argparse import ArgumentParser
|
||||
from typing import Dict
|
||||
from typing import Final
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
@@ -18,11 +18,14 @@ from github import GithubContainerRegistryApi
|
||||
logger = logging.getLogger("cleanup-tags")
|
||||
|
||||
|
||||
class DockerManifest2:
|
||||
class ImageProperties:
|
||||
"""
|
||||
Data class wrapping the Docker Image Manifest Version 2.
|
||||
Data class wrapping the properties of an entry in the image index
|
||||
manifests list. It is NOT an actual image with layers, etc
|
||||
|
||||
See https://docs.docker.com/registry/spec/manifest-v2-2/
|
||||
https://docs.docker.com/registry/spec/manifest-v2-2/
|
||||
https://github.com/opencontainers/image-spec/blob/main/manifest.md
|
||||
https://github.com/opencontainers/image-spec/blob/main/descriptor.md
|
||||
"""
|
||||
|
||||
def __init__(self, data: Dict) -> None:
|
||||
@@ -39,6 +42,45 @@ class DockerManifest2:
|
||||
self.platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
|
||||
|
||||
|
||||
class ImageIndex:
|
||||
"""
|
||||
Data class wrapping up logic for an OCI Image Index
|
||||
JSON data. Primary use is to access the manifests listing
|
||||
|
||||
See https://github.com/opencontainers/image-spec/blob/main/image-index.md
|
||||
"""
|
||||
|
||||
def __init__(self, package_url: str, tag: str) -> None:
|
||||
self.qualified_name = f"{package_url}:{tag}"
|
||||
logger.info(f"Getting image index for {self.qualified_name}")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[
|
||||
shutil.which("docker"),
|
||||
"buildx",
|
||||
"imagetools",
|
||||
"inspect",
|
||||
"--raw",
|
||||
self.qualified_name,
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
self._data = json.loads(proc.stdout)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
f"Failed to get image index for {self.qualified_name}: {e.stderr}",
|
||||
)
|
||||
raise e
|
||||
|
||||
@property
|
||||
def image_pointers(self) -> Iterator[ImageProperties]:
|
||||
for manifest_data in self._data["manifests"]:
|
||||
yield ImageProperties(manifest_data)
|
||||
|
||||
|
||||
class RegistryTagsCleaner:
|
||||
"""
|
||||
This is the base class for the image registry cleaning. Given a package
|
||||
@@ -85,7 +127,10 @@ class RegistryTagsCleaner:
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
This method will delete image versions, based on the selected tags to delete
|
||||
This method will delete image versions, based on the selected tags to delete.
|
||||
It behaves more like an unlinking than actual deletion. Removing the tag
|
||||
simply removes a pointer to an image, but the actual image data remains accessible
|
||||
if one has the sha256 digest of it.
|
||||
"""
|
||||
for tag_to_delete in self.tags_to_delete:
|
||||
package_version_info = self.all_pkgs_tags_to_version[tag_to_delete]
|
||||
@@ -149,27 +194,17 @@ class RegistryTagsCleaner:
|
||||
|
||||
# Parse manifests to locate digests pointed to
|
||||
for tag in sorted(self.tags_to_keep):
|
||||
full_name = f"ghcr.io/{self.repo_owner}/{self.package_name}:{tag}"
|
||||
logger.info(f"Checking manifest for {full_name}")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[
|
||||
shutil.which("docker"),
|
||||
"manifest",
|
||||
"inspect",
|
||||
full_name,
|
||||
],
|
||||
capture_output=True,
|
||||
image_index = ImageIndex(
|
||||
f"ghcr.io/{self.repo_owner}/{self.package_name}",
|
||||
tag,
|
||||
)
|
||||
|
||||
manifest_list = json.loads(proc.stdout)
|
||||
for manifest_data in manifest_list["manifests"]:
|
||||
manifest = DockerManifest2(manifest_data)
|
||||
for manifest in image_index.image_pointers:
|
||||
|
||||
if manifest.digest in untagged_versions:
|
||||
logger.info(
|
||||
f"Skipping deletion of {manifest.digest},"
|
||||
f" referred to by {full_name}"
|
||||
f" referred to by {image_index.qualified_name}"
|
||||
f" for {manifest.platform}",
|
||||
)
|
||||
del untagged_versions[manifest.digest]
|
||||
@@ -241,6 +276,55 @@ class RegistryTagsCleaner:
|
||||
# By default, keep anything which is tagged
|
||||
self.tags_to_keep = list(set(self.all_pkgs_tags_to_version.keys()))
|
||||
|
||||
def check_remaining_tags_valid(self):
|
||||
"""
|
||||
Checks the non-deleted tags are still valid. The assumption is if the
|
||||
manifest is can be inspected and each image manifest if points to can be
|
||||
inspected, the image will still pull.
|
||||
|
||||
https://github.com/opencontainers/image-spec/blob/main/image-index.md
|
||||
"""
|
||||
logger.info("Beginning confirmation step")
|
||||
a_tag_failed = False
|
||||
for tag in sorted(self.tags_to_keep):
|
||||
|
||||
try:
|
||||
image_index = ImageIndex(
|
||||
f"ghcr.io/{self.repo_owner}/{self.package_name}",
|
||||
tag,
|
||||
)
|
||||
for manifest in image_index.image_pointers:
|
||||
logger.info(f"Checking {manifest.digest} for {manifest.platform}")
|
||||
|
||||
# This follows the pointer from the index to an actual image, layers and all
|
||||
# Note the format is @
|
||||
digest_name = f"ghcr.io/{self.repo_owner}/{self.package_name}@{manifest.digest}"
|
||||
|
||||
try:
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
shutil.which("docker"),
|
||||
"buildx",
|
||||
"imagetools",
|
||||
"inspect",
|
||||
"--raw",
|
||||
digest_name,
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to inspect digest: {e.stderr}")
|
||||
a_tag_failed = True
|
||||
except subprocess.CalledProcessError as e:
|
||||
a_tag_failed = True
|
||||
logger.error(f"Failed to inspect: {e.stderr}")
|
||||
continue
|
||||
|
||||
if a_tag_failed:
|
||||
raise Exception("At least one image tag failed to inspect")
|
||||
|
||||
|
||||
class MainImageTagsCleaner(RegistryTagsCleaner):
|
||||
def decide_what_tags_to_keep(self):
|
||||
@@ -301,12 +385,10 @@ class MainImageTagsCleaner(RegistryTagsCleaner):
|
||||
|
||||
class LibraryTagsCleaner(RegistryTagsCleaner):
|
||||
"""
|
||||
Exists for the off change that someday, the installer library images
|
||||
Exists for the off chance that someday, the installer library images
|
||||
will need their own logic
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _main():
|
||||
parser = ArgumentParser(
|
||||
@@ -397,6 +479,10 @@ def _main():
|
||||
# Clean images which are untagged
|
||||
cleaner.clean_untagged(args.is_manifest)
|
||||
|
||||
# Verify remaining tags still pull
|
||||
if args.is_manifest:
|
||||
cleaner.check_remaining_tags_valid()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
|
1
.github/scripts/common.py
vendored
1
.github/scripts/common.py
vendored
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
import logging
|
||||
|
||||
|
||||
|
1
.github/scripts/get-build-json.py
vendored
Executable file → Normal file
1
.github/scripts/get-build-json.py
vendored
Executable file → Normal file
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
This is a helper script for the mutli-stage Docker image builder.
|
||||
It provides a single point of configuration for package version control.
|
||||
|
6
.github/scripts/github.py
vendored
6
.github/scripts/github.py
vendored
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
This module contains some useful classes for interacting with the Github API.
|
||||
The full documentation for the API can be found here: https://docs.github.com/en/rest
|
||||
@@ -162,10 +161,7 @@ class ContainerPackage(_EndpointResponse):
|
||||
Returns True if the image has at least one tag which matches the given regex,
|
||||
False otherwise
|
||||
"""
|
||||
for tag in self.tags:
|
||||
if re.match(pattern, tag) is not None:
|
||||
return True
|
||||
return False
|
||||
return any(re.match(pattern, tag) is not None for tag in self.tags)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Package {self.name}"
|
||||
|
108
.github/workflows/ci.yml
vendored
108
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2022.11.30"
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.3.20"
|
||||
# This is the default version of Python to use in most steps
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PYTHON_VERSION: "3.9"
|
||||
@@ -113,16 +113,12 @@ jobs:
|
||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
# Skip Tests which require convert
|
||||
PAPERLESS_TEST_SKIP_CONVERT: 1
|
||||
# Enable Gotenberg end to end testing
|
||||
GOTENBERG_LIVE: 1
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Start containers
|
||||
run: |
|
||||
@@ -145,6 +141,10 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript libzbar0 poppler-utils
|
||||
-
|
||||
name: Configure ImageMagick
|
||||
run: |
|
||||
sudo cp docker/imagemagick-policy.xml /etc/ImageMagick-6/policy.xml
|
||||
-
|
||||
name: Install Python dependencies
|
||||
run: |
|
||||
@@ -160,27 +160,14 @@ jobs:
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pytest -ra
|
||||
-
|
||||
name: Get changed files
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v35
|
||||
name: Upload coverage to Codecov
|
||||
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
|
||||
uses: codecov/codecov-action@v3
|
||||
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
|
||||
if: matrix.python-version == ${{ env.DEFAULT_PYTHON_VERSION }} && steps.changed-files-specific.outputs.any_changed == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# https://github.com/coveralls-clients/coveralls-python/issues/251
|
||||
run: |
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run coveralls --service=github
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# future expansion
|
||||
flags: backend
|
||||
-
|
||||
name: Stop containers
|
||||
if: always()
|
||||
@@ -203,6 +190,8 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- run: cd src-ui && npm ci
|
||||
- run: cd src-ui && npm run lint
|
||||
- run: cd src-ui && npm run test
|
||||
@@ -212,12 +201,6 @@ jobs:
|
||||
name: Prepare Docker Pipeline Data
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||
runs-on: ubuntu-22.04
|
||||
# If the push triggered the installer library workflow, wait for it to
|
||||
# complete here. This ensures the required versions for the final
|
||||
# image have been built, while not waiting at all if the versions haven't changed
|
||||
concurrency:
|
||||
group: build-installer-library
|
||||
cancel-in-progress: false
|
||||
needs:
|
||||
- documentation
|
||||
- tests-backend
|
||||
@@ -320,6 +303,7 @@ jobs:
|
||||
images: |
|
||||
ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}
|
||||
name=paperlessngx/paperless-ngx,enable=${{ steps.docker-hub.outputs.enable }}
|
||||
name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.docker-hub.outputs.enable }}
|
||||
tags: |
|
||||
# Tag branches with branch name
|
||||
type=ref,event=branch
|
||||
@@ -351,9 +335,18 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Login to Quay.io
|
||||
uses: docker/login-action@v2
|
||||
# Don't attempt to login is not pushing to Docker Hub
|
||||
if: steps.docker-hub.outputs.enable == 'true'
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -448,21 +441,48 @@ jobs:
|
||||
-
|
||||
name: Move files
|
||||
run: |
|
||||
mkdir dist
|
||||
mkdir dist/paperless-ngx
|
||||
mkdir dist/paperless-ngx/scripts
|
||||
cp .dockerignore .env Dockerfile Pipfile Pipfile.lock requirements.txt LICENSE README.md dist/paperless-ngx/
|
||||
cp paperless.conf.example dist/paperless-ngx/paperless.conf
|
||||
cp gunicorn.conf.py dist/paperless-ngx/gunicorn.conf.py
|
||||
cp -r docker/ dist/paperless-ngx/docker
|
||||
cp scripts/*.service scripts/*.sh scripts/*.socket dist/paperless-ngx/scripts/
|
||||
cp -r src/ dist/paperless-ngx/src
|
||||
cp -r docs/_build/html/ dist/paperless-ngx/docs
|
||||
mv static dist/paperless-ngx
|
||||
echo "Making dist folders"
|
||||
for directory in dist \
|
||||
dist/paperless-ngx \
|
||||
dist/paperless-ngx/scripts;
|
||||
do
|
||||
mkdir --verbose --parents ${directory}
|
||||
done
|
||||
|
||||
echo "Copying basic files"
|
||||
for file_name in .dockerignore \
|
||||
.env \
|
||||
Dockerfile \
|
||||
Pipfile \
|
||||
Pipfile.lock \
|
||||
requirements.txt \
|
||||
LICENSE \
|
||||
README.md \
|
||||
paperless.conf.example \
|
||||
gunicorn.conf.py
|
||||
do
|
||||
cp --verbose ${file_name} dist/paperless-ngx/
|
||||
done
|
||||
mv --verbose dist/paperless-ngx/paperless.conf.example dist/paperless-ngx/paperless.conf
|
||||
|
||||
echo "Copying Docker related files"
|
||||
cp --recursive docker/ dist/paperless-ngx/docker
|
||||
|
||||
echo "Copying startup scripts"
|
||||
cp --verbose scripts/*.service scripts/*.sh scripts/*.socket dist/paperless-ngx/scripts/
|
||||
|
||||
echo "Copying source files"
|
||||
cp --recursive src/ dist/paperless-ngx/src
|
||||
echo "Copying documentation"
|
||||
cp --recursive docs/_build/html/ dist/paperless-ngx/docs
|
||||
|
||||
mv --verbose static dist/paperless-ngx
|
||||
-
|
||||
name: Make release package
|
||||
run: |
|
||||
echo "Creating release archive"
|
||||
cd dist
|
||||
sudo chown -R 1000:1000 paperless-ngx/
|
||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||
-
|
||||
name: Upload release artifact
|
||||
@@ -578,5 +598,5 @@ jobs:
|
||||
owner,
|
||||
repo,
|
||||
issue_number: result.data.number,
|
||||
labels: ['documentation']
|
||||
labels: ['documentation', 'skip-changelog']
|
||||
});
|
||||
|
14
.github/workflows/cleanup-tags.yml
vendored
14
.github/workflows/cleanup-tags.yml
vendored
@@ -62,9 +62,9 @@ jobs:
|
||||
with:
|
||||
python-version: "3.10"
|
||||
-
|
||||
name: Install httpx
|
||||
name: Install Python libraries
|
||||
run: |
|
||||
python -m pip install httpx
|
||||
python -m pip install httpx docker
|
||||
#
|
||||
# Clean up primary package
|
||||
#
|
||||
@@ -81,13 +81,3 @@ jobs:
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
run: |
|
||||
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --untagged --delete "${{ matrix.cache-name }}"
|
||||
#
|
||||
# Verify tags which are left still pull
|
||||
#
|
||||
-
|
||||
name: Check all tags still pull
|
||||
run: |
|
||||
ghcr_name=$(echo "ghcr.io/${GITHUB_REPOSITORY_OWNER}/${{ matrix.primary-name }}" | awk '{ print tolower($0) }')
|
||||
echo "Pulling all tags of ${ghcr_name}"
|
||||
docker pull --quiet --all-tags ${ghcr_name}
|
||||
docker image list
|
||||
|
139
.github/workflows/installer-library.yml
vendored
139
.github/workflows/installer-library.yml
vendored
@@ -169,3 +169,142 @@ jobs:
|
||||
PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
|
||||
PILLOW_VERSION=${{ needs.prepare-docker-build.outputs.pillow-version }}
|
||||
LXML_VERSION=${{ needs.prepare-docker-build.outputs.lxml-version }}
|
||||
|
||||
commit-binary-files:
|
||||
name: Store installers
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
- build-qpdf-debs
|
||||
- build-jbig2enc
|
||||
- build-psycopg2-wheel
|
||||
- build-pikepdf-wheel
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: binary-library
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
-
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -qq --no-install-recommends tree
|
||||
-
|
||||
name: Extract qpdf files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).image_tag }}
|
||||
|
||||
docker pull --quiet ${tag}
|
||||
docker create --name qpdf-extract ${tag}
|
||||
|
||||
mkdir --parents qpdf/${version}/amd64
|
||||
docker cp qpdf-extract:/usr/src/qpdf/${version}/amd64 qpdf/${version}
|
||||
|
||||
mkdir --parents qpdf/${version}/arm64
|
||||
docker cp qpdf-extract:/usr/src/qpdf/${version}/arm64 qpdf/${version}
|
||||
|
||||
mkdir --parents qpdf/${version}/armv7
|
||||
docker cp qpdf-extract:/usr/src/qpdf/${version}/armv7 qpdf/${version}
|
||||
-
|
||||
name: Extract psycopg2 files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).image_tag }}
|
||||
|
||||
docker pull --quiet --platform linux/amd64 ${tag}
|
||||
docker create --platform linux/amd64 --name psycopg2-extract ${tag}
|
||||
mkdir --parents psycopg2/${version}/amd64
|
||||
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/amd64
|
||||
mv psycopg2/${version}/amd64/wheels/* psycopg2/${version}/amd64
|
||||
rm -r psycopg2/${version}/amd64/wheels/
|
||||
docker rm psycopg2-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm64 ${tag}
|
||||
docker create --platform linux/arm64 --name psycopg2-extract ${tag}
|
||||
mkdir --parents psycopg2/${version}/arm64
|
||||
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/arm64
|
||||
mv psycopg2/${version}/arm64/wheels/* psycopg2/${version}/arm64
|
||||
rm -r psycopg2/${version}/arm64/wheels/
|
||||
docker rm psycopg2-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm/v7 ${tag}
|
||||
docker create --platform linux/arm/v7 --name psycopg2-extract ${tag}
|
||||
mkdir --parents psycopg2/${version}/armv7
|
||||
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/armv7
|
||||
mv psycopg2/${version}/armv7/wheels/* psycopg2/${version}/armv7
|
||||
rm -r psycopg2/${version}/armv7/wheels/
|
||||
docker rm psycopg2-extract
|
||||
-
|
||||
name: Extract pikepdf files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).image_tag }}
|
||||
|
||||
docker pull --quiet --platform linux/amd64 ${tag}
|
||||
docker create --platform linux/amd64 --name pikepdf-extract ${tag}
|
||||
mkdir --parents pikepdf/${version}/amd64
|
||||
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/amd64
|
||||
mv pikepdf/${version}/amd64/wheels/* pikepdf/${version}/amd64
|
||||
rm -r pikepdf/${version}/amd64/wheels/
|
||||
docker rm pikepdf-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm64 ${tag}
|
||||
docker create --platform linux/arm64 --name pikepdf-extract ${tag}
|
||||
mkdir --parents pikepdf/${version}/arm64
|
||||
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/arm64
|
||||
mv pikepdf/${version}/arm64/wheels/* pikepdf/${version}/arm64
|
||||
rm -r pikepdf/${version}/arm64/wheels/
|
||||
docker rm pikepdf-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm/v7 ${tag}
|
||||
docker create --platform linux/arm/v7 --name pikepdf-extract ${tag}
|
||||
mkdir --parents pikepdf/${version}/armv7
|
||||
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/armv7
|
||||
mv pikepdf/${version}/armv7/wheels/* pikepdf/${version}/armv7
|
||||
rm -r pikepdf/${version}/armv7/wheels/
|
||||
docker rm pikepdf-extract
|
||||
-
|
||||
name: Extract jbig2enc files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).image_tag }}
|
||||
|
||||
docker pull --quiet --platform linux/amd64 ${tag}
|
||||
docker create --platform linux/amd64 --name jbig2enc-extract ${tag}
|
||||
mkdir --parents jbig2enc/${version}/amd64
|
||||
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/amd64/
|
||||
mv jbig2enc/${version}/amd64/build/* jbig2enc/${version}/amd64/
|
||||
docker rm jbig2enc-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm64 ${tag}
|
||||
docker create --platform linux/arm64 --name jbig2enc-extract ${tag}
|
||||
mkdir --parents jbig2enc/${version}/arm64
|
||||
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/arm64
|
||||
mv jbig2enc/${version}/arm64/build/* jbig2enc/${version}/arm64/
|
||||
docker rm jbig2enc-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm/v7 ${tag}
|
||||
docker create --platform linux/arm/v7 --name jbig2enc-extract ${tag}
|
||||
mkdir --parents jbig2enc/${version}/armv7
|
||||
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/armv7
|
||||
mv jbig2enc/${version}/armv7/build/* jbig2enc/${version}/armv7/
|
||||
docker rm jbig2enc-extract
|
||||
-
|
||||
name: Show file structure
|
||||
run: |
|
||||
tree .
|
||||
-
|
||||
name: Commit files
|
||||
run: |
|
||||
git config --global user.name "github-actions"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add pikepdf/ qpdf/ psycopg2/ jbig2enc/
|
||||
git commit -m "Updating installer packages" || true
|
||||
git push origin || true
|
||||
|
4
.github/workflows/project-actions.yml
vendored
4
.github/workflows/project-actions.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||
steps:
|
||||
- name: Add issue to project and set status to ${{ env.todo }}
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.0.1
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
with:
|
||||
gh_token: ${{ secrets.GH_TOKEN }}
|
||||
organization: paperless-ngx
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||
steps:
|
||||
- name: Add PR to project and set status to "Needs Review"
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.0.1
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
with:
|
||||
gh_token: ${{ secrets.GH_TOKEN }}
|
||||
organization: paperless-ngx
|
||||
|
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Build ${{ fromJSON(inputs.build-json).name }}
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -73,6 +73,7 @@ virtualenv
|
||||
.venv/
|
||||
/docker-compose.env
|
||||
/docker-compose.yml
|
||||
.ruff_cache/
|
||||
|
||||
# Used for development
|
||||
scripts/import-for-development
|
||||
|
@@ -36,39 +36,14 @@ repos:
|
||||
- markdown
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.9.0
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.259'
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
exclude: "(migrations)"
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: "v1.4.0"
|
||||
hooks:
|
||||
- id: yesqa
|
||||
exclude: "(migrations)"
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
rev: "v2.4.0"
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
exclude: "(migrations)"
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: ^src/
|
||||
args:
|
||||
- "--config=./src/setup.cfg"
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
exclude: "(migrations)"
|
||||
args:
|
||||
- "--py38-plus"
|
||||
# Dockerfile hooks
|
||||
- repo: https://github.com/AleksaC/hadolint-py
|
||||
rev: v2.10.0
|
||||
|
@@ -1 +1 @@
|
||||
3.8.15
|
||||
3.8.16
|
||||
|
23
.ruff.toml
Normal file
23
.ruff.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
# https://beta.ruff.rs/docs/settings/
|
||||
# https://beta.ruff.rs/docs/rules/
|
||||
select = ["F", "E", "W", "UP", "COM", "DJ", "EXE", "ISC", "ICN", "G201", "INP", "PIE", "RSE", "SIM", "TID", "PLC", "PLE", "RUF"]
|
||||
# TODO PTH
|
||||
ignore = ["DJ001", "SIM105"]
|
||||
fix = true
|
||||
line-length = 88
|
||||
respect-gitignore = true
|
||||
src = ["src"]
|
||||
target-version = "py38"
|
||||
format = "grouped"
|
||||
show-fixes = true
|
||||
|
||||
[per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"docker/wait-for-redis.py" = ["INP001"]
|
||||
"*/tests/*.py" = ["E501", "SIM117"]
|
||||
"*/migrations/*.py" = ["E501", "SIM"]
|
||||
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001"]
|
||||
"src/documents/models.py" = ["SIM115"]
|
||||
|
||||
[isort]
|
||||
force-single-line = true
|
106
Dockerfile
106
Dockerfile
@@ -1,26 +1,12 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md
|
||||
|
||||
# Pull the installer images from the library
|
||||
# These are all built previously
|
||||
# They provide either a .deb or .whl
|
||||
|
||||
ARG JBIG2ENC_VERSION
|
||||
ARG QPDF_VERSION
|
||||
ARG PIKEPDF_VERSION
|
||||
ARG PSYCOPG2_VERSION
|
||||
|
||||
FROM ghcr.io/paperless-ngx/paperless-ngx/builder/jbig2enc:${JBIG2ENC_VERSION} as jbig2enc-builder
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/paperless-ngx/paperless-ngx/builder/qpdf:${QPDF_VERSION} as qpdf-builder
|
||||
FROM ghcr.io/paperless-ngx/paperless-ngx/builder/pikepdf:${PIKEPDF_VERSION} as pikepdf-builder
|
||||
FROM ghcr.io/paperless-ngx/paperless-ngx/builder/psycopg2:${PSYCOPG2_VERSION} as psycopg2-builder
|
||||
|
||||
# Stage: compile-frontend
|
||||
# Purpose: Compiles the frontend
|
||||
# Notes:
|
||||
# - Does NPM stuff with Typescript and such
|
||||
FROM --platform=$BUILDPLATFORM node:16-bullseye-slim AS compile-frontend
|
||||
|
||||
# This stage compiles the frontend
|
||||
# This stage runs once for the native platform, as the outputs are not
|
||||
# dependent on target arch
|
||||
# Inputs: None
|
||||
|
||||
COPY ./src-ui /src/src-ui
|
||||
|
||||
WORKDIR /src/src-ui
|
||||
@@ -30,25 +16,27 @@ RUN set -eux \
|
||||
RUN set -eux \
|
||||
&& ./node_modules/.bin/ng build --configuration production
|
||||
|
||||
# Stage: pipenv-base
|
||||
# Purpose: Generates a requirements.txt file for building
|
||||
# Comments:
|
||||
# - pipenv dependencies are not left in the final image
|
||||
# - pipenv can't touch the final image somehow
|
||||
FROM --platform=$BUILDPLATFORM python:3.9-slim-bullseye as pipenv-base
|
||||
|
||||
# This stage generates the requirements.txt file using pipenv
|
||||
# This stage runs once for the native platform, as the outputs are not
|
||||
# dependent on target arch
|
||||
# This way, pipenv dependencies are not left in the final image
|
||||
# nor can pipenv mess up the final image somehow
|
||||
# Inputs: None
|
||||
|
||||
WORKDIR /usr/src/pipenv
|
||||
|
||||
COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2022.11.30 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.3.20 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
# Stage: main-app
|
||||
# Purpose: The final image
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here
|
||||
FROM python:3.9-slim-bullseye as main-app
|
||||
|
||||
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
||||
@@ -58,30 +46,14 @@ LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-n
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# Buildx provided
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
# Workflow provided
|
||||
ARG QPDF_VERSION
|
||||
|
||||
#
|
||||
# Begin installation and configuration
|
||||
# Order the steps below from least often changed to most
|
||||
#
|
||||
|
||||
# copy jbig2enc
|
||||
# Basically will never change again
|
||||
COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/
|
||||
COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/jbig2 /usr/local/bin/
|
||||
COPY --from=jbig2enc-builder /usr/src/jbig2enc/src/*.h /usr/local/include/
|
||||
|
||||
# Packages need for running
|
||||
ARG RUNTIME_PACKAGES="\
|
||||
# Python
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
# General utils
|
||||
curl \
|
||||
# Docker specific
|
||||
@@ -145,7 +117,7 @@ RUN set -eux \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "Installing supervisor" \
|
||||
&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor==4.2.4
|
||||
&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor==4.2.5
|
||||
|
||||
# Copy gunicorn config
|
||||
# Changes very infrequently
|
||||
@@ -154,7 +126,6 @@ WORKDIR /usr/src/paperless/
|
||||
COPY gunicorn.conf.py .
|
||||
|
||||
# setup docker-specific things
|
||||
# Use mounts to avoid copying installer files into the image
|
||||
# These change sometimes, but rarely
|
||||
WORKDIR /usr/src/paperless/src/docker/
|
||||
|
||||
@@ -195,22 +166,42 @@ RUN set -eux \
|
||||
&& chmod +x install_management_commands.sh \
|
||||
&& ./install_management_commands.sh
|
||||
|
||||
# Buildx provided, must be defined to use though
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
# Workflow provided, defaults set for manual building
|
||||
ARG JBIG2ENC_VERSION=0.29
|
||||
ARG QPDF_VERSION=11.3.0
|
||||
ARG PIKEPDF_VERSION=7.1.1
|
||||
ARG PSYCOPG2_VERSION=2.9.5
|
||||
|
||||
# Install the built packages from the installer library images
|
||||
# Use mounts to avoid copying installer files into the image
|
||||
# These change sometimes
|
||||
RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \
|
||||
--mount=type=bind,from=psycopg2-builder,target=/psycopg2 \
|
||||
--mount=type=bind,from=pikepdf-builder,target=/pikepdf \
|
||||
set -eux \
|
||||
RUN set -eux \
|
||||
&& echo "Getting binaries" \
|
||||
&& mkdir paperless-ngx \
|
||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/paperless-ngx/archive/ba28a1e16c27d121b644b4f6bdb78855a2850561.tar.gz \
|
||||
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
|
||||
&& cd paperless-ngx \
|
||||
# Setting a specific revision ensures we know what this installed
|
||||
# and ensures cache breaking on changes
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& cp ./jbig2enc/${JBIG2ENC_VERSION}/${TARGETARCH}${TARGETVARIANT}/jbig2 /usr/local/bin/ \
|
||||
&& cp ./jbig2enc/${JBIG2ENC_VERSION}/${TARGETARCH}${TARGETVARIANT}/libjbig2enc* /usr/local/lib/ \
|
||||
&& echo "Installing qpdf" \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/libqpdf29_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/qpdf_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends ./qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/libqpdf29_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends ./qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/qpdf_*.deb \
|
||||
&& echo "Installing pikepdf and dependencies" \
|
||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/*.whl \
|
||||
&& python3 -m pip install --no-cache-dir ./pikepdf/${PIKEPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.whl \
|
||||
&& python3 -m pip list \
|
||||
&& echo "Installing psycopg2" \
|
||||
&& python3 -m pip install --no-cache-dir /psycopg2/usr/src/wheels/psycopg2*.whl \
|
||||
&& python3 -m pip list
|
||||
&& python3 -m pip install --no-cache-dir ./psycopg2/${PSYCOPG2_VERSION}/${TARGETARCH}${TARGETVARIANT}/psycopg2*.whl \
|
||||
&& python3 -m pip list \
|
||||
&& echo "Cleaning up image layer" \
|
||||
&& cd ../ \
|
||||
&& rm -rf paperless-ngx \
|
||||
&& rm paperless-ngx.tar.gz
|
||||
|
||||
WORKDIR /usr/src/paperless/src/
|
||||
|
||||
@@ -254,11 +245,12 @@ COPY ./src ./
|
||||
COPY --from=compile-frontend /src/src/documents/static/frontend/ ./documents/static/frontend/
|
||||
|
||||
# add users, setup scripts
|
||||
# Mount the compiled frontend to expected location
|
||||
RUN set -eux \
|
||||
&& addgroup --gid 1000 paperless \
|
||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||
&& chown -R paperless:paperless ../ \
|
||||
&& gosu paperless python3 manage.py collectstatic --clear --no-input \
|
||||
&& chown -R paperless:paperless /usr/src/paperless \
|
||||
&& gosu paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||
&& gosu paperless python3 manage.py compilemessages
|
||||
|
||||
VOLUME ["/usr/src/paperless/data", \
|
||||
|
46
Pipfile
46
Pipfile
@@ -12,15 +12,20 @@ name = "piwheels"
|
||||
dateparser = "~=1.1"
|
||||
django = "~=4.1"
|
||||
django-cors-headers = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-guardian = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=22.1"
|
||||
djangorestframework = "~=3.14"
|
||||
djangorestframework-guardian = "*"
|
||||
django-ipware = "*"
|
||||
filelock = "*"
|
||||
gunicorn = "*"
|
||||
imap-tools = "*"
|
||||
langdetect = "*"
|
||||
pathvalidate = "*"
|
||||
pillow = "~=9.3"
|
||||
pillow = "~=9.4"
|
||||
pikepdf = "*"
|
||||
python-gnupg = "*"
|
||||
python-dotenv = "*"
|
||||
@@ -29,10 +34,10 @@ python-magic = "*"
|
||||
psycopg2 = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.1"
|
||||
scikit-learn = "~=1.2"
|
||||
numpy = "*"
|
||||
whitenoise = "~=6.2"
|
||||
watchdog = "~=2.1"
|
||||
whitenoise = "~=6.3"
|
||||
watchdog = "~=2.2"
|
||||
whoosh="~=2.7"
|
||||
inotifyrecursive = "~=0.3"
|
||||
ocrmypdf = "~=14.0"
|
||||
@@ -41,33 +46,25 @@ tika = "*"
|
||||
# TODO: This will sadly also install daphne+dependencies,
|
||||
# which an ASGI server we don't need. Adds about 15MB image size.
|
||||
channels = "~=3.0"
|
||||
channels-redis = "*"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
concurrent-log-handler = "*"
|
||||
"pdfminer.six" = "*"
|
||||
pyzbar = "*"
|
||||
mysqlclient = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
django-celery-results = "*"
|
||||
setproctitle = "*"
|
||||
nltk = "*"
|
||||
pdf2image = "*"
|
||||
flower = "*"
|
||||
bleach = "*"
|
||||
|
||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
#
|
||||
# Packages locked due to issues (try to check if these are fixed in a release every so often)
|
||||
#
|
||||
|
||||
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
||||
scipy = "==1.8.1"
|
||||
|
||||
# Newer versions aren't builting yet (see https://www.piwheels.org/project/cryptography/)
|
||||
cryptography = "==38.0.1"
|
||||
|
||||
# Locked version until https://github.com/django/channels_redis/issues/332
|
||||
# is resolved
|
||||
channels-redis = "==3.4.1"
|
||||
|
||||
[dev-packages]
|
||||
coveralls = "*"
|
||||
factory-boy = "*"
|
||||
@@ -81,3 +78,24 @@ black = "*"
|
||||
pre-commit = "*"
|
||||
imagehash = "*"
|
||||
mkdocs-material = "*"
|
||||
ruff = "*"
|
||||
|
||||
[typing-dev]
|
||||
mypy = "*"
|
||||
types-Pillow = "*"
|
||||
django-filter-stubs = "*"
|
||||
types-python-dateutil = "*"
|
||||
djangorestframework-stubs = {extras= ["compatible-mypy"], version="*"}
|
||||
celery-types = "*"
|
||||
django-stubs = {extras= ["compatible-mypy"], version="*"}
|
||||
types-dateparser = "*"
|
||||
types-bleach = "*"
|
||||
types-humanfriendly = "*"
|
||||
types-redis = "*"
|
||||
types-tqdm = "*"
|
||||
types-Markdown = "*"
|
||||
types-Pygments = "*"
|
||||
types-backports = "*"
|
||||
types-colorama = "*"
|
||||
types-psycopg2 = "*"
|
||||
types-setuptools = "*"
|
||||
|
2431
Pipfile.lock
generated
2431
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
17
README.md
17
README.md
@@ -1,7 +1,7 @@
|
||||
[](https://github.com/paperless-ngx/paperless-ngx/actions)
|
||||
[](https://crowdin.com/project/paperless-ngx)
|
||||
[](https://docs.paperless-ngx.com)
|
||||
[](https://coveralls.io/github/paperless-ngx/paperless-ngx?branch=master)
|
||||
[](https://codecov.io/gh/paperless-ngx/paperless-ngx)
|
||||
[](https://matrix.to/#/%23paperlessngx%3Amatrix.org)
|
||||
[](https://demo.paperless-ngx.com)
|
||||
|
||||
@@ -101,13 +101,16 @@ For bugs please [open an issue](https://github.com/paperless-ngx/paperless-ngx/i
|
||||
|
||||
# Affiliated Projects
|
||||
|
||||
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 for a while now, and people have built tools that interact with it. If you're one of them, please reach out and we can add your project to the list. Current projects include:
|
||||
|
||||
- [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ng.
|
||||
- [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.
|
||||
- [Paperless Mobile](https://github.com/astubenbord/paperless-mobile): A modern, feature rich mobile application for Paperless.
|
||||
- **Mobile**
|
||||
- [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS application for Paperless-ngx.
|
||||
- [Paperless Mobile](https://github.com/astubenbord/paperless-mobile): A modern, feature rich Android app for Paperless-ngx.
|
||||
- [Paperless Share](https://github.com/qcasey/paperless_share): Share any files from your Android application with Paperless-ngx. Very simple, but works with all mobile scanning apps that allow you to share scanned documents.
|
||||
- **Desktop**
|
||||
- [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for use in Paperless-ngx.
|
||||
|
||||
# Important Note
|
||||
|
||||
Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption. This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home.
|
||||
> Document scanners are typically used to scan sensitive documents like your social insurance number, tax records, invoices, etc. **Paperless-ngx should never be run on an untrusted host** because information is stored in clear text without encryption. No guarantees are made regarding security (but we do try!) and you use the app at your own risk.
|
||||
> **The safest way to run Paperless-ngx is on a local server in your own home with backups in place**.
|
||||
|
@@ -29,7 +29,20 @@ RUN set -eux \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
&& make \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./pkg-list.txt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "Moving files around" \
|
||||
&& mkdir build \
|
||||
# Unlink a symlink that causes problems
|
||||
&& unlink ./src/.libs/libjbig2enc.la \
|
||||
# Move what the link pointed to
|
||||
&& mv ./src/libjbig2enc.la ./build/ \
|
||||
# Move the shared library .so files
|
||||
&& mv ./src/.libs/libjbig2enc* ./build/ \
|
||||
# And move the cli binary
|
||||
&& mv ./src/jbig2 ./build/ \
|
||||
&& mv ./pkg-list.txt ./build/
|
||||
|
@@ -7,12 +7,17 @@
|
||||
# Default to pulling from the main repo registry when manually building
|
||||
ARG REPO="paperless-ngx/paperless-ngx"
|
||||
|
||||
# This does nothing, except provide a name for a copy below
|
||||
ARG QPDF_VERSION
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/${REPO}/builder/qpdf:${QPDF_VERSION} as qpdf-builder
|
||||
|
||||
# This does nothing, except provide a name for a copy below
|
||||
|
||||
FROM python:3.9-slim-bullseye as main
|
||||
#
|
||||
# Stage: builder
|
||||
# Purpose:
|
||||
# - Build the pikepdf wheel
|
||||
# - Build any dependent wheels which can't be found
|
||||
#
|
||||
FROM python:3.9-slim-bullseye as builder
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with pikepdf wheel built"
|
||||
|
||||
@@ -100,3 +105,14 @@ RUN set -eux \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#
|
||||
# Stage: package
|
||||
# Purpose: Holds the compiled .whl files in a tiny image to pull
|
||||
#
|
||||
FROM alpine:3.17 as package
|
||||
|
||||
WORKDIR /usr/src/wheels/
|
||||
|
||||
COPY --from=builder /usr/src/wheels/*.whl ./
|
||||
COPY --from=builder /usr/src/wheels/pkg-list.txt ./
|
||||
|
@@ -2,7 +2,12 @@
|
||||
# Inputs:
|
||||
# - PSYCOPG2_VERSION - Version to build
|
||||
|
||||
FROM python:3.9-slim-bullseye as main
|
||||
#
|
||||
# Stage: builder
|
||||
# Purpose:
|
||||
# - Build the psycopg2 wheel
|
||||
#
|
||||
FROM python:3.9-slim-bullseye as builder
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with psycopg2 wheel built"
|
||||
|
||||
@@ -48,3 +53,14 @@ RUN set -eux \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#
|
||||
# Stage: package
|
||||
# Purpose: Holds the compiled .whl files in a tiny image to pull
|
||||
#
|
||||
FROM alpine:3.17 as package
|
||||
|
||||
WORKDIR /usr/src/wheels/
|
||||
|
||||
COPY --from=builder /usr/src/wheels/*.whl ./
|
||||
COPY --from=builder /usr/src/wheels/pkg-list.txt ./
|
||||
|
57
docker-builders/README.md
Normal file
57
docker-builders/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Installer Library
|
||||
|
||||
This folder contains the Dockerfiles for building certain installers or libraries, which are then pulled into the main image.
|
||||
|
||||
## [jbig2enc](https://github.com/agl/jbig2enc)
|
||||
|
||||
### Why
|
||||
|
||||
JBIG is an image coding which can achieve better compression of images for PDFs.
|
||||
|
||||
### What
|
||||
|
||||
The Docker image builds a shared library file and utility, which is copied into the correct location in the final image.
|
||||
|
||||
### Updating
|
||||
|
||||
1. Ensure the given qpdf version is present in [Debian bookworm](https://packages.debian.org/bookworm/qpdf)
|
||||
2. Update `.build-config.json` to the given version
|
||||
3. If the Debian specific version has incremented, update `Dockerfile.qpdf`
|
||||
|
||||
See Also:
|
||||
|
||||
- [OCRMyPDF Documentation](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html)
|
||||
|
||||
## [psycopg2](https://www.psycopg.org/)
|
||||
|
||||
### Why
|
||||
|
||||
The pre-built wheels of psycopg2 are built on Debian 9, which provides a quite old version of libpq-dev. This causes issue with authentication methods.
|
||||
|
||||
### What
|
||||
|
||||
The image builds psycopg2 wheels on Debian 10 and places the produced wheels into `/usr/src/wheels/`.
|
||||
|
||||
See Also:
|
||||
|
||||
- [Issue 266](https://github.com/paperless-ngx/paperless-ngx/issues/266)
|
||||
|
||||
## [qpdf](https://qpdf.readthedocs.io/en/stable/index.html)
|
||||
|
||||
### Why
|
||||
|
||||
qpdf and it's library provide tools to read, manipulate and fix up PDFs. Version 11 is also required by `pikepdf` 6+ and Debian 9 does not provide above version 10.
|
||||
|
||||
### What
|
||||
|
||||
The Docker image cross compiles .deb installers for each supported architecture of the main image. The installers are placed in `/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/`
|
||||
|
||||
## [pikepdf](https://pikepdf.readthedocs.io/en/latest/)
|
||||
|
||||
### Why
|
||||
|
||||
Required by OCRMyPdf, this is a general purpose library for PDF manipulation in Python via the qpdf libraries.
|
||||
|
||||
### What
|
||||
|
||||
The built wheels are placed into `/usr/src/wheels/`
|
@@ -6,7 +6,7 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
hostname: gotenberg
|
||||
container_name: gotenberg
|
||||
network_mode: host
|
||||
|
@@ -59,7 +59,7 @@ services:
|
||||
- gotenberg
|
||||
- tika
|
||||
ports:
|
||||
- 8000:8000
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
|
@@ -53,7 +53,7 @@ services:
|
||||
- db
|
||||
- broker
|
||||
ports:
|
||||
- 8000:8000
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
|
@@ -53,7 +53,7 @@ services:
|
||||
- db
|
||||
- broker
|
||||
ports:
|
||||
- 8010:8000
|
||||
- "8010:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
|
@@ -57,7 +57,7 @@ services:
|
||||
- gotenberg
|
||||
- tika
|
||||
ports:
|
||||
- 8000:8000
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
@@ -77,7 +77,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
|
@@ -51,7 +51,7 @@ services:
|
||||
- db
|
||||
- broker
|
||||
ports:
|
||||
- 8000:8000
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
|
@@ -46,7 +46,7 @@ services:
|
||||
- gotenberg
|
||||
- tika
|
||||
ports:
|
||||
- 8000:8000
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.6
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
|
@@ -37,7 +37,7 @@ services:
|
||||
depends_on:
|
||||
- broker
|
||||
ports:
|
||||
- 8000:8000
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
|
@@ -80,7 +80,7 @@ django_checks() {
|
||||
|
||||
search_index() {
|
||||
|
||||
local -r index_version=1
|
||||
local -r index_version=4
|
||||
local -r index_version_file=${DATA_DIR}/.index_version
|
||||
|
||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||
|
@@ -3,5 +3,10 @@
|
||||
echo "Checking if we should start flower..."
|
||||
|
||||
if [[ -n "${PAPERLESS_ENABLE_FLOWER}" ]]; then
|
||||
celery --app paperless flower
|
||||
# Small delay to allow celery to be up first
|
||||
echo "Starting flower in 5s"
|
||||
sleep 5
|
||||
celery --app paperless flower --conf=/usr/src/paperless/src/paperless/flowerconfig.py
|
||||
else
|
||||
echo "Not starting flower"
|
||||
fi
|
||||
|
@@ -28,7 +28,7 @@ stderr_logfile_maxbytes=0
|
||||
|
||||
[program:celery]
|
||||
|
||||
command = celery --app paperless worker --loglevel INFO
|
||||
command = celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
|
||||
user=paperless
|
||||
stopasgroup = true
|
||||
stopwaitsecs = 60
|
||||
|
@@ -18,7 +18,7 @@ if __name__ == "__main__":
|
||||
|
||||
REDIS_URL: Final[str] = os.getenv("PAPERLESS_REDIS", "redis://localhost:6379")
|
||||
|
||||
print(f"Waiting for Redis...", flush=True)
|
||||
print("Waiting for Redis...", flush=True)
|
||||
|
||||
attempt = 0
|
||||
with Redis.from_url(url=REDIS_URL) as client:
|
||||
@@ -37,8 +37,8 @@ if __name__ == "__main__":
|
||||
attempt += 1
|
||||
|
||||
if attempt >= MAX_RETRY_COUNT:
|
||||
print(f"Failed to connect to redis using environment variable PAPERLESS_REDIS.")
|
||||
print("Failed to connect to redis using environment variable PAPERLESS_REDIS.")
|
||||
sys.exit(os.EX_UNAVAILABLE)
|
||||
else:
|
||||
print(f"Connected to Redis broker.")
|
||||
print("Connected to Redis broker.")
|
||||
sys.exit(os.EX_OK)
|
||||
|
@@ -98,7 +98,7 @@ the background.
|
||||
won't automatically update to newer versions. In order to enable
|
||||
updates as described above, either get the new `docker-compose.yml`
|
||||
file from
|
||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/master/docker/compose)
|
||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||
or edit the `docker-compose.yml` file, find the line that says
|
||||
|
||||
```
|
||||
@@ -475,12 +475,13 @@ mail_fetcher
|
||||
The command takes no arguments and processes all your mail accounts and
|
||||
rules.
|
||||
|
||||
!!! note
|
||||
!!! tip
|
||||
|
||||
As of October 2022 Microsoft no longer supports IMAP authentication
|
||||
for Exchange servers, thus Exchange is no longer supported until a
|
||||
solution is implemented in the Python IMAP library used by Paperless.
|
||||
See [learn.microsoft.com](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online)
|
||||
To use OAuth access tokens for mail fetching,
|
||||
select the box to indicate the password is actually
|
||||
a token when creating or editing a mail account. The
|
||||
details for creating a token depend on your email
|
||||
provider.
|
||||
|
||||
### Creating archived documents {#archiver}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ Paperless will compare the matching algorithms defined by every tag,
|
||||
correspondent, document type, and storage path in your database to see
|
||||
if they apply to the text in a document. In other words, if you define a
|
||||
tag called `Home Utility` that had a `match` property of `bc hydro` and
|
||||
a `matching_algorithm` of `literal`, Paperless will automatically tag
|
||||
a `matching_algorithm` of `Exact`, Paperless will automatically tag
|
||||
your newly-consumed document with your `Home Utility` tag so long as the
|
||||
text `bc hydro` appears in the body of the document somewhere.
|
||||
|
||||
@@ -25,12 +25,13 @@ documents.
|
||||
|
||||
The following algorithms are available:
|
||||
|
||||
- **None:** No matching will be performed.
|
||||
- **Any:** Looks for any occurrence of any word provided in match in
|
||||
the PDF. If you define the match as `Bank1 Bank2`, it will match
|
||||
documents containing either of these terms.
|
||||
- **All:** Requires that every word provided appears in the PDF,
|
||||
albeit not in the order provided.
|
||||
- **Literal:** Matches only if the match appears exactly as provided
|
||||
- **Exact:** Matches only if the match appears exactly as provided
|
||||
(i.e. preserve ordering) in the PDF.
|
||||
- **Regular expression:** Parses the match as a regular expression and
|
||||
tries to find a match within the document.
|
||||
@@ -121,7 +122,17 @@ Executed after the consumer sees a new document in the consumption
|
||||
folder, but before any processing of the document is performed. This
|
||||
script can access the following relevant environment variables set:
|
||||
|
||||
- `DOCUMENT_SOURCE_PATH`
|
||||
| Environment Variable | Description |
|
||||
| ----------------------- | ------------------------------------------------------------ |
|
||||
| `DOCUMENT_SOURCE_PATH` | Original path of the consumed document |
|
||||
| `DOCUMENT_WORKING_PATH` | Path to a copy of the original that consumption will work on |
|
||||
|
||||
!!! note
|
||||
|
||||
Pre-consume scripts which modify the document should only change
|
||||
the `DOCUMENT_WORKING_PATH` file or a second consume task may
|
||||
be triggered, leading to failures as two tasks work on the
|
||||
same document path
|
||||
|
||||
A simple but common example for this would be creating a simple script
|
||||
like this:
|
||||
@@ -130,7 +141,7 @@ like this:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
pdf2pdfocr.py -i ${DOCUMENT_SOURCE_PATH}
|
||||
pdf2pdfocr.py -i ${DOCUMENT_WORKING_PATH}
|
||||
```
|
||||
|
||||
`/etc/paperless.conf`
|
||||
@@ -157,26 +168,36 @@ Executed after the consumer has successfully processed a document and
|
||||
has moved it into paperless. It receives the following environment
|
||||
variables:
|
||||
|
||||
- `DOCUMENT_ID`
|
||||
- `DOCUMENT_FILE_NAME`
|
||||
- `DOCUMENT_CREATED`
|
||||
- `DOCUMENT_MODIFIED`
|
||||
- `DOCUMENT_ADDED`
|
||||
- `DOCUMENT_SOURCE_PATH`
|
||||
- `DOCUMENT_ARCHIVE_PATH`
|
||||
- `DOCUMENT_THUMBNAIL_PATH`
|
||||
- `DOCUMENT_DOWNLOAD_URL`
|
||||
- `DOCUMENT_THUMBNAIL_URL`
|
||||
- `DOCUMENT_CORRESPONDENT`
|
||||
- `DOCUMENT_TAGS`
|
||||
- `DOCUMENT_ORIGINAL_FILENAME`
|
||||
| Environment Variable | Description |
|
||||
| ---------------------------- | --------------------------------------------- |
|
||||
| `DOCUMENT_ID` | Database primary key of the document |
|
||||
| `DOCUMENT_FILE_NAME` | Formatted filename, not including paths |
|
||||
| `DOCUMENT_CREATED` | Date & time when document created |
|
||||
| `DOCUMENT_MODIFIED` | Date & time when document was last modified |
|
||||
| `DOCUMENT_ADDED` | Date & time when document was added |
|
||||
| `DOCUMENT_SOURCE_PATH` | Path to the original document file |
|
||||
| `DOCUMENT_ARCHIVE_PATH` | Path to the generate archive file (if any) |
|
||||
| `DOCUMENT_THUMBNAIL_PATH` | Path to the generated thumbnail |
|
||||
| `DOCUMENT_DOWNLOAD_URL` | URL for document download |
|
||||
| `DOCUMENT_THUMBNAIL_URL` | URL for the document thumbnail |
|
||||
| `DOCUMENT_CORRESPONDENT` | Assigned correspondent (if any) |
|
||||
| `DOCUMENT_TAGS` | Comma separated list of tags applied (if any) |
|
||||
| `DOCUMENT_ORIGINAL_FILENAME` | Filename of original document |
|
||||
|
||||
The script can be in any language, but for a simple shell script
|
||||
example, you can take a look at
|
||||
[post-consumption-example.sh](https://github.com/paperless-ngx/paperless-ngx/blob/main/scripts/post-consumption-example.sh)
|
||||
in this project.
|
||||
The script can be in any language, A simple shell script example:
|
||||
|
||||
The post consumption script cannot cancel the consumption process.
|
||||
```bash title="post-consumption-example"
|
||||
--8<-- "./scripts/post-consumption-example.sh"
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
The post consumption script cannot cancel the consumption process.
|
||||
|
||||
!!! warning
|
||||
|
||||
The post consumption script should not modify the document files
|
||||
directly
|
||||
|
||||
The script's stdout and stderr will be logged line by line to the
|
||||
webserver log, along with the exit code of the script.
|
||||
@@ -288,6 +309,8 @@ Paperless provides the following placeholders within filenames:
|
||||
- `{added_month_name_short}`: Month added abbreviated name, as per
|
||||
locale
|
||||
- `{added_day}`: Day added only (number 01-31).
|
||||
- `{owner_username}`: Username of document owner, if any, or "none"
|
||||
- `{original_name}`: Document original filename, minus the extension, if any, or "none"
|
||||
|
||||
Paperless will try to conserve the information from your database as
|
||||
much as possible. However, some characters that you can use in document
|
||||
@@ -348,7 +371,7 @@ value.
|
||||
One of the best things in Paperless is that you can not only access the
|
||||
documents via the web interface, but also via the file system.
|
||||
|
||||
When as single storage layout is not sufficient for your use case,
|
||||
When a single storage layout is not sufficient for your use case,
|
||||
storage paths come to the rescue. Storage paths allow you to configure
|
||||
more precisely where each document is stored in the file system.
|
||||
|
||||
@@ -394,13 +417,6 @@ structure as in the previous example above.
|
||||
Defining a storage path is optional. If no storage path is defined for a
|
||||
document, the global `PAPERLESS_FILENAME_FORMAT` is applied.
|
||||
|
||||
!!! warning
|
||||
|
||||
If you adjust the format of an existing storage path, old documents
|
||||
don't get relocated automatically. You need to run the
|
||||
[document renamer](/administration#renamer) to
|
||||
adjust their paths.
|
||||
|
||||
## Celery Monitoring {#celery-monitoring}
|
||||
|
||||
The monitoring tool
|
||||
@@ -481,3 +497,49 @@ You can also set the default for new tables (this does NOT affect
|
||||
existing tables) with:
|
||||
|
||||
`ALTER DATABASE <db_name> CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`
|
||||
|
||||
!!! warning
|
||||
|
||||
Using mariadb version 10.4+ is recommended. Using the `utf8mb3` character set on
|
||||
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||
|
||||
## Barcodes {#barcodes}
|
||||
|
||||
Paperless is able to utilize barcodes for automatically preforming some tasks.
|
||||
|
||||
At this time, the library utilized for detection of bacodes supports the following types:
|
||||
|
||||
- AN-13/UPC-A
|
||||
- UPC-E
|
||||
- EAN-8
|
||||
- Code 128
|
||||
- Code 93
|
||||
- Code 39
|
||||
- Codabar
|
||||
- Interleaved 2 of 5
|
||||
- QR Code
|
||||
- SQ Code
|
||||
|
||||
You may check for updates on the [zbar library homepage](https://github.com/mchehab/zbar).
|
||||
For usage in Paperless, the type of barcode does not matter, only the contents of it.
|
||||
|
||||
For how to enable barcode usage, see [the configuration](/configuration#barcodes).
|
||||
The two settings may be enabled independently, but do have interactions as explained
|
||||
below.
|
||||
|
||||
### Document Splitting
|
||||
|
||||
When enabled, Paperless will look for a barcode with the configured value and create a new document
|
||||
starting from the next page. The page with the barcode on it will _not_ be retained. It
|
||||
is expected to be a page existing only for triggering the split.
|
||||
|
||||
### Archive Serial Number Assignment
|
||||
|
||||
When enabled, the value of the barcode (as an integer) will be used to set the document's
|
||||
archive serial number, allowing quick reference back to the original, paper document.
|
||||
|
||||
If document splitting via barcode is also enabled, documents will be split when an ASN
|
||||
barcode is located. However, differing from the splitting, the page with the
|
||||
barcode _will_ be retained. This allows application of a barcode to any page, including
|
||||
one which holds data to keep in the document.
|
||||
|
14
docs/api.md
14
docs/api.md
@@ -16,6 +16,8 @@ The API provides 7 main endpoints:
|
||||
- `/api/tags/`: Full CRUD support.
|
||||
- `/api/mail_accounts/`: Full CRUD support.
|
||||
- `/api/mail_rules/`: Full CRUD support.
|
||||
- `/api/users/`: Full CRUD support.
|
||||
- `/api/groups/`: Full CRUD support.
|
||||
|
||||
All of these endpoints except for the logging endpoint allow you to
|
||||
fetch, edit and delete individual objects by appending their primary key
|
||||
@@ -254,11 +256,15 @@ The endpoint supports the following optional form fields:
|
||||
- `document_type`: Similar to correspondent.
|
||||
- `tags`: Similar to correspondent. Specify this multiple times to
|
||||
have multiple tags added to the document.
|
||||
- `owner`: An optional user ID to set as the owner.
|
||||
- `archive_serial_number`: An optional archive serial number to set.
|
||||
|
||||
The endpoint will immediately return "OK" if the document consumption
|
||||
process was started successfully. No additional status information about
|
||||
the consumption process itself is available, since that happens in a
|
||||
different process.
|
||||
The endpoint will immediately return HTTP 200 if the document consumption
|
||||
process was started successfully, with the UUID of the consumption task
|
||||
as the data. No additional status information about
|
||||
the consumption process itself is available immediately, since that happens in a
|
||||
different process. Querying the tasks endpoint with the returned UUID will
|
||||
provide information on the state of the consumption.
|
||||
|
||||
## API Versioning
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 890 KiB |
@@ -1,5 +1,99 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 1.13.0
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: allow disable warn on close saved view with changes [@shamoon](https://github.com/shamoon) ([#2681](https://github.com/paperless-ngx/paperless-ngx/pull/2681))
|
||||
- Feature: Add option to enable response compression [@stumpylog](https://github.com/stumpylog) ([#2621](https://github.com/paperless-ngx/paperless-ngx/pull/2621))
|
||||
- Feature: split documents on ASN barcode [@muued](https://github.com/muued) ([#2554](https://github.com/paperless-ngx/paperless-ngx/pull/2554))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Ignore path filtering didn't handle sub directories [@stumpylog](https://github.com/stumpylog) ([#2674](https://github.com/paperless-ngx/paperless-ngx/pull/2674))
|
||||
- Bugfix: Generation of secret key hangs during install script [@stumpylog](https://github.com/stumpylog) ([#2657](https://github.com/paperless-ngx/paperless-ngx/pull/2657))
|
||||
- Fix: Remove files produced by barcode splitting when completed [@stumpylog](https://github.com/stumpylog) ([#2648](https://github.com/paperless-ngx/paperless-ngx/pull/2648))
|
||||
- Fix: add missing storage path placeholders [@shamoon](https://github.com/shamoon) ([#2651](https://github.com/paperless-ngx/paperless-ngx/pull/2651))
|
||||
- Fix long dropdown contents break document detail column view [@shamoon](https://github.com/shamoon) ([#2638](https://github.com/paperless-ngx/paperless-ngx/pull/2638))
|
||||
- Fix: tags dropdown should stay closed when removing [@shamoon](https://github.com/shamoon) ([#2625](https://github.com/paperless-ngx/paperless-ngx/pull/2625))
|
||||
- Bugfix: Configure scheduled tasks to expire after some time [@stumpylog](https://github.com/stumpylog) ([#2614](https://github.com/paperless-ngx/paperless-ngx/pull/2614))
|
||||
- Bugfix: Limit management list pagination maxSize to 5 [@Kaaybi](https://github.com/Kaaybi) ([#2618](https://github.com/paperless-ngx/paperless-ngx/pull/2618))
|
||||
- Fix: Don't crash on bad ASNs during indexing [@stumpylog](https://github.com/stumpylog) ([#2586](https://github.com/paperless-ngx/paperless-ngx/pull/2586))
|
||||
- Fix: Prevent mktime OverflowError except in even more rare caes [@stumpylog](https://github.com/stumpylog) ([#2574](https://github.com/paperless-ngx/paperless-ngx/pull/2574))
|
||||
- Bugfix: Whoosh relative date queries weren't handling timezones [@stumpylog](https://github.com/stumpylog) ([#2566](https://github.com/paperless-ngx/paperless-ngx/pull/2566))
|
||||
- Fix importing files with non-ascii names [@Kexogg](https://github.com/Kexogg) ([#2555](https://github.com/paperless-ngx/paperless-ngx/pull/2555))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Chore: update recommended Gotenberg to 7.8, docs note possible incompatibility [@shamoon](https://github.com/shamoon) ([#2608](https://github.com/paperless-ngx/paperless-ngx/pull/2608))
|
||||
- [Documentation] Add v1.12.2 changelog [@github-actions](https://github.com/github-actions) ([#2553](https://github.com/paperless-ngx/paperless-ngx/pull/2553))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Faster Docker image cleanup [@stumpylog](https://github.com/stumpylog) ([#2687](https://github.com/paperless-ngx/paperless-ngx/pull/2687))
|
||||
- Chore: Remove duplicated folder [@stumpylog](https://github.com/stumpylog) ([#2561](https://github.com/paperless-ngx/paperless-ngx/pull/2561))
|
||||
- Chore: Switch test coverage to Codecov [@stumpylog](https://github.com/stumpylog) ([#2582](https://github.com/paperless-ngx/paperless-ngx/pull/2582))
|
||||
- Bump docker/build-push-action from 3 to 4 [@dependabot](https://github.com/dependabot) ([#2576](https://github.com/paperless-ngx/paperless-ngx/pull/2576))
|
||||
- Chore: Run tests which require convert in the CI [@stumpylog](https://github.com/stumpylog) ([#2570](https://github.com/paperless-ngx/paperless-ngx/pull/2570))
|
||||
|
||||
- Feature: split documents on ASN barcode [@muued](https://github.com/muued) ([#2554](https://github.com/paperless-ngx/paperless-ngx/pull/2554))
|
||||
- Bugfix: Whoosh relative date queries weren't handling timezones [@stumpylog](https://github.com/stumpylog) ([#2566](https://github.com/paperless-ngx/paperless-ngx/pull/2566))
|
||||
- Fix importing files with non-ascii names [@Kexogg](https://github.com/Kexogg) ([#2555](https://github.com/paperless-ngx/paperless-ngx/pull/2555))
|
||||
|
||||
## paperless-ngx 1.12.2
|
||||
|
||||
_Note: Version 1.12.x introduced searching of comments which will work for comments added after the upgrade but a reindex of the search index is required in order to be able to search
|
||||
older comments. The Docker image will automatically perform this reindex, bare metal installations will have to perform this manually, see [the docs](https://docs.paperless-ngx.com/administration/#index)._
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Bugfix: Allow pre-consume scripts to modify incoming file [@stumpylog](https://github.com/stumpylog) ([#2547](https://github.com/paperless-ngx/paperless-ngx/pull/2547))
|
||||
- Bugfix: Return to page based barcode scanning [@stumpylog](https://github.com/stumpylog) ([#2544](https://github.com/paperless-ngx/paperless-ngx/pull/2544))
|
||||
- Fix: Try to prevent title debounce overwriting [@shamoon](https://github.com/shamoon) ([#2543](https://github.com/paperless-ngx/paperless-ngx/pull/2543))
|
||||
- Fix comment search highlight + multi-word search [@shamoon](https://github.com/shamoon) ([#2542](https://github.com/paperless-ngx/paperless-ngx/pull/2542))
|
||||
- Bugfix: Request PDF/A format from Gotenberg [@stumpylog](https://github.com/stumpylog) ([#2530](https://github.com/paperless-ngx/paperless-ngx/pull/2530))
|
||||
- Fix: Trigger reindex for pre-existing comments [@shamoon](https://github.com/shamoon) ([#2519](https://github.com/paperless-ngx/paperless-ngx/pull/2519))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Bugfix: Allow pre-consume scripts to modify incoming file [@stumpylog](https://github.com/stumpylog) ([#2547](https://github.com/paperless-ngx/paperless-ngx/pull/2547))
|
||||
- Fix: Trigger reindex for pre-existing comments [@shamoon](https://github.com/shamoon) ([#2519](https://github.com/paperless-ngx/paperless-ngx/pull/2519))
|
||||
- Minor updates to development documentation [@clemensrieder](https://github.com/clemensrieder) ([#2474](https://github.com/paperless-ngx/paperless-ngx/pull/2474))
|
||||
- [Documentation] Add v1.12.1 changelog [@github-actions](https://github.com/github-actions) ([#2515](https://github.com/paperless-ngx/paperless-ngx/pull/2515))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Fix tag cleaner to work with attestations [@stumpylog](https://github.com/stumpylog) ([#2532](https://github.com/paperless-ngx/paperless-ngx/pull/2532))
|
||||
- Chore: Make installers statically versioned [@stumpylog](https://github.com/stumpylog) ([#2517](https://github.com/paperless-ngx/paperless-ngx/pull/2517))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Bugfix: Allow pre-consume scripts to modify incoming file [@stumpylog](https://github.com/stumpylog) ([#2547](https://github.com/paperless-ngx/paperless-ngx/pull/2547))
|
||||
- Bugfix: Return to page based barcode scanning [@stumpylog](https://github.com/stumpylog) ([#2544](https://github.com/paperless-ngx/paperless-ngx/pull/2544))
|
||||
- Fix: Try to prevent title debounce overwriting [@shamoon](https://github.com/shamoon) ([#2543](https://github.com/paperless-ngx/paperless-ngx/pull/2543))
|
||||
- Fix comment search highlight + multi-word search [@shamoon](https://github.com/shamoon) ([#2542](https://github.com/paperless-ngx/paperless-ngx/pull/2542))
|
||||
- Bugfix: Request PDF/A format from Gotenberg [@stumpylog](https://github.com/stumpylog) ([#2530](https://github.com/paperless-ngx/paperless-ngx/pull/2530))
|
||||
|
||||
## paperless-ngx 1.12.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: comments not showing in search until after manual reindex in v1.12 [@shamoon](https://github.com/shamoon) ([#2513](https://github.com/paperless-ngx/paperless-ngx/pull/2513))
|
||||
- Fix: date range search broken in 1.12 [@shamoon](https://github.com/shamoon) ([#2509](https://github.com/paperless-ngx/paperless-ngx/pull/2509))
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Documentation] Add v1.12.0 changelog [@github-actions](https://github.com/github-actions) ([#2507](https://github.com/paperless-ngx/paperless-ngx/pull/2507))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Moves back to the main release-drafter now that it does what we wanted [@stumpylog](https://github.com/stumpylog) ([#2503](https://github.com/paperless-ngx/paperless-ngx/pull/2503))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Fix: comments not showing in search until after manual reindex in v1.12 [@shamoon](https://github.com/shamoon) ([#2513](https://github.com/paperless-ngx/paperless-ngx/pull/2513))
|
||||
- Fix: date range search broken in 1.12 [@shamoon](https://github.com/shamoon) ([#2509](https://github.com/paperless-ngx/paperless-ngx/pull/2509))
|
||||
|
||||
## paperless-ngx 1.12.0
|
||||
|
||||
### Features
|
||||
|
@@ -17,6 +17,8 @@ run paperless, these settings have to be defined in different places.
|
||||
|
||||
## Required services
|
||||
|
||||
### Redis Broker
|
||||
|
||||
`PAPERLESS_REDIS=<url>`
|
||||
|
||||
: This is required for processing scheduled tasks such as email
|
||||
@@ -33,6 +35,8 @@ matcher.
|
||||
|
||||
Defaults to `redis://localhost:6379`.
|
||||
|
||||
### Database
|
||||
|
||||
`PAPERLESS_DBENGINE=<engine_name>`
|
||||
|
||||
: Optional, gives the ability to choose Postgres or MariaDB for
|
||||
@@ -86,6 +90,36 @@ changed here.
|
||||
|
||||
Default is `prefer`.
|
||||
|
||||
`PAPERLESS_DBSSLROOTCERT=<ca-path>`
|
||||
|
||||
: SSL root certificate path
|
||||
|
||||
See [the official documentation about
|
||||
sslmode](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||
Changes path of `root.crt`.
|
||||
|
||||
Defaults to unset, using the documented path in the home directory.
|
||||
|
||||
`PAPERLESS_DBSSLCERT=<client-cert-path>`
|
||||
|
||||
: SSL client certificate path
|
||||
|
||||
See [the official documentation about
|
||||
sslmode](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||
Changes path of `postgresql.crt`.
|
||||
|
||||
Defaults to unset, using the documented path in the home directory.
|
||||
|
||||
`PAPERLESS_DBSSLKEY=<client-cert-key>`
|
||||
|
||||
: SSL client key path
|
||||
|
||||
See [the official documentation about
|
||||
sslmode](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||
Changes path of `postgresql.key`.
|
||||
|
||||
Defaults to unset, using the documented path in the home directory.
|
||||
|
||||
`PAPERLESS_DB_TIMEOUT=<float>`
|
||||
|
||||
: Amount of time for a database connection to wait for the database to
|
||||
@@ -94,6 +128,47 @@ changing to postgresql if you need to increase this.
|
||||
|
||||
Defaults to unset, keeping the Django defaults.
|
||||
|
||||
## Optional Services
|
||||
|
||||
### Tika {#tika}
|
||||
|
||||
Paperless can make use of [Tika](https://tika.apache.org/) and
|
||||
[Gotenberg](https://gotenberg.dev/) for parsing and converting
|
||||
"Office" documents (such as ".doc", ".xlsx" and ".odt").
|
||||
Tika and Gotenberg are also needed to allow parsing of E-Mails (.eml).
|
||||
|
||||
If you wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||
configure their endpoints, and enable the feature.
|
||||
|
||||
`PAPERLESS_TIKA_ENABLED=<bool>`
|
||||
|
||||
: Enable (or disable) the Tika parser.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_TIKA_ENDPOINT=<url>`
|
||||
|
||||
: Set the endpoint URL were Paperless can reach your Tika server.
|
||||
|
||||
Defaults to "<http://localhost:9998>".
|
||||
|
||||
`PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>`
|
||||
|
||||
: Set the endpoint URL were Paperless can reach your Gotenberg server.
|
||||
|
||||
Defaults to "<http://localhost:3000>".
|
||||
|
||||
If you run paperless on docker, you can add those services to the
|
||||
docker-compose file (see the provided
|
||||
[`docker-compose.sqlite-tika.yml`](https://github.com/paperless-ngx/paperless-ngx/blob/main/docker/compose/docker-compose.sqlite-tika.yml)
|
||||
file for reference).
|
||||
|
||||
Add all three configuration parameters to your configuration. If using
|
||||
Docker, this may be the `environment` key of the webserver or a
|
||||
`docker-compose.env` file. Bare metal installations may have a `.conf` file
|
||||
containing the configuration parameters. Be sure to use the correct format
|
||||
and watch out for indentation if editing the YAML file.
|
||||
|
||||
## Paths and folders
|
||||
|
||||
`PAPERLESS_CONSUMPTION_DIR=<path>`
|
||||
@@ -141,7 +216,8 @@ directory.
|
||||
files created using "collectstatic" manager command are stored.
|
||||
|
||||
Unless you're doing something fancy, there is no need to override
|
||||
this.
|
||||
this. If this is changed, you may need to run
|
||||
`collectstatic` again.
|
||||
|
||||
Defaults to "../static/", relative to the "src" directory.
|
||||
|
||||
@@ -226,8 +302,7 @@ not include a trailing slash. E.g. <https://paperless.domain.com>
|
||||
|
||||
: A list of trusted origins for unsafe requests (e.g. POST). As of
|
||||
Django 4.0 this is required to access the Django admin via the web.
|
||||
See
|
||||
<https://docs.djangoproject.com/en/4.0/ref/settings/#csrf-trusted-origins>
|
||||
See the [Django project documentation on the settings](https://docs.djangoproject.com/en/4.1/ref/settings/#csrf-trusted-origins)
|
||||
|
||||
Can also be set using PAPERLESS_URL (see above).
|
||||
|
||||
@@ -238,8 +313,8 @@ See
|
||||
|
||||
: If you're planning on putting Paperless on the open internet, then
|
||||
you really should set this value to the domain name you're using.
|
||||
Failing to do so leaves you open to HTTP host header attacks:
|
||||
<https://docs.djangoproject.com/en/3.1/topics/security/#host-header-validation>
|
||||
Failing to do so leaves you open to HTTP host header attacks.
|
||||
You can read more about this in [the Django project's documentation](https://docs.djangoproject.com/en/4.1/topics/security/#host-header-validation)
|
||||
|
||||
Just remember that this is a comma-separated list, so
|
||||
"example.com" is fine, as is "example.com,www.example.com", but
|
||||
@@ -261,6 +336,14 @@ do CORS calls. Set this to your public domain name.
|
||||
|
||||
Defaults to "<http://localhost:8000>".
|
||||
|
||||
`PAPERLESS_TRUSTED_PROXIES=<comma-separated-list>`
|
||||
|
||||
: This may be needed to prevent IP address spoofing if you are using e.g.
|
||||
fail2ban with log entries for failed authorization attempts. Value should be
|
||||
IP address(es).
|
||||
|
||||
Defaults to empty string.
|
||||
|
||||
`PAPERLESS_FORCE_SCRIPT_NAME=<path>`
|
||||
|
||||
: To host paperless under a subpath url like example.com/paperless you
|
||||
@@ -347,16 +430,16 @@ applications.
|
||||
If you're exposing paperless to the internet directly, do not use
|
||||
this.
|
||||
|
||||
Also see the warning [in the official documentation](https://docs.djangoproject.com/en/3.1/howto/auth-remote-user/#configuration).
|
||||
Also see the warning [in the official documentation](https://docs.djangoproject.com/en/4.1/howto/auth-remote-user/#configuration).
|
||||
|
||||
Defaults to "false" which disables this feature.
|
||||
|
||||
`PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=<str>`
|
||||
|
||||
: If "PAPERLESS*ENABLE_HTTP_REMOTE_USER" is enabled, this
|
||||
: If "PAPERLESS_ENABLE_HTTP_REMOTE_USER" is enabled, this
|
||||
property allows to customize the name of the HTTP header from which
|
||||
the authenticated username is extracted. Values are in terms of
|
||||
[HttpRequest.META](https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.META).
|
||||
[HttpRequest.META](https://docs.djangoproject.com/en/4.1/ref/request-response/#django.http.HttpRequest.META).
|
||||
Thus, the configured value must start with `HTTP*`
|
||||
followed by the normalized actual header name.
|
||||
|
||||
@@ -382,21 +465,20 @@ needs.
|
||||
: Customize the language that paperless will attempt to use when
|
||||
parsing documents.
|
||||
|
||||
It should be a 3-letter language code consistent with ISO 639:
|
||||
https://www.loc.gov/standards/iso639-2/php/code_list.php
|
||||
It should be a 3-letter code, see the list of [languages Tesseract supports](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html).
|
||||
|
||||
Set this to the language most of your documents are written in.
|
||||
|
||||
This can be a combination of multiple languages such as `deu+eng`,
|
||||
in which case tesseract will use whatever language matches best.
|
||||
Keep in mind that tesseract uses much more cpu time with multiple
|
||||
in which case Tesseract will use whatever language matches best.
|
||||
Keep in mind that Tesseract uses much more CPU time with multiple
|
||||
languages enabled.
|
||||
|
||||
Defaults to "eng".
|
||||
|
||||
!!! note
|
||||
|
||||
If your language contains a '-' such as chi-sim, you must use chi_sim
|
||||
If your language contains a '-' such as chi-sim, you must use `chi_sim`.
|
||||
|
||||
`PAPERLESS_OCR_MODE=<mode>`
|
||||
|
||||
@@ -406,12 +488,6 @@ modes are available:
|
||||
- `skip`: Paperless skips all pages and will perform ocr only on
|
||||
pages where no text is present. This is the safest option.
|
||||
|
||||
- `skip_noarchive`: In addition to skip, paperless won't create
|
||||
an archived version of your documents when it finds any text in
|
||||
them. This is useful if you don't want to have two
|
||||
almost-identical versions of your digital documents in the media
|
||||
folder. This is the fastest option.
|
||||
|
||||
- `redo`: Paperless will OCR all pages of your documents and
|
||||
attempt to replace any existing text layers with new text. This
|
||||
will be useful for documents from scanners that already
|
||||
@@ -434,6 +510,19 @@ modes are available:
|
||||
Read more about this in the [OCRmyPDF
|
||||
documentation](https://ocrmypdf.readthedocs.io/en/latest/advanced.html#when-ocr-is-skipped).
|
||||
|
||||
`PAPERLESS_OCR_SKIP_ARCHIVE_FILE=<mode>`
|
||||
|
||||
: Specify when you would like paperless to skip creating an archived
|
||||
version of your documents. This is useful if you don't want to have two
|
||||
almost-identical versions of your documents in the media folder.
|
||||
|
||||
- `never`: Never skip creating an archived version.
|
||||
- `with_text`: Skip creating an archived version for documents
|
||||
that already have embedded text.
|
||||
- `always`: Always skip creating an archived version.
|
||||
|
||||
The default is `never`.
|
||||
|
||||
`PAPERLESS_OCR_CLEAN=<mode>`
|
||||
|
||||
: Tells paperless to use `unpaper` to clean any input document before
|
||||
@@ -576,76 +665,6 @@ they use underscores instead of dashes.
|
||||
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
|
||||
```
|
||||
|
||||
## Tika settings {#tika}
|
||||
|
||||
Paperless can make use of [Tika](https://tika.apache.org/) and
|
||||
[Gotenberg](https://gotenberg.dev/) for parsing and converting
|
||||
"Office" documents (such as ".doc", ".xlsx" and ".odt").
|
||||
Tika and Gotenberg are also needed to allow parsing of E-Mails (.eml).
|
||||
|
||||
If you wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||
configure their endpoints, and enable the feature.
|
||||
|
||||
`PAPERLESS_TIKA_ENABLED=<bool>`
|
||||
|
||||
: Enable (or disable) the Tika parser.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_TIKA_ENDPOINT=<url>`
|
||||
|
||||
: Set the endpoint URL were Paperless can reach your Tika server.
|
||||
|
||||
Defaults to "<http://localhost:9998>".
|
||||
|
||||
`PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>`
|
||||
|
||||
: Set the endpoint URL were Paperless can reach your Gotenberg server.
|
||||
|
||||
Defaults to "<http://localhost:3000>".
|
||||
|
||||
If you run paperless on docker, you can add those services to the
|
||||
docker-compose file (see the provided `docker-compose.sqlite-tika.yml`
|
||||
file for reference). The changes requires are as follows:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# ...
|
||||
|
||||
webserver:
|
||||
# ...
|
||||
|
||||
environment:
|
||||
# ...
|
||||
|
||||
PAPERLESS_TIKA_ENABLED: 1
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
# ...
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7.6
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
command:
|
||||
- 'gotenberg'
|
||||
- '--chromium-disable-javascript=true'
|
||||
- '--chromium-allow-list=file:///tmp/.*'
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Add the configuration variables to the environment of the webserver
|
||||
(alternatively put the configuration in the `docker-compose.env` file)
|
||||
and add the additional 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}
|
||||
|
||||
`PAPERLESS_TASK_WORKERS=<num>`
|
||||
@@ -697,17 +716,10 @@ paperless will process in parallel on a single document.
|
||||
on large documents within the default 1800 seconds. So extending
|
||||
this timeout may prove to be useful on weak hardware setups.
|
||||
|
||||
`PAPERLESS_WORKER_RETRY=<num>`
|
||||
|
||||
: If PAPERLESS_WORKER_TIMEOUT has been configured, the retry time for
|
||||
a task can also be configured. By default, this value will be set to
|
||||
10s more than the worker timeout. This value should never be set
|
||||
less than the worker timeout.
|
||||
|
||||
`PAPERLESS_TIME_ZONE=<timezone>`
|
||||
|
||||
: Set the time zone here. See
|
||||
<https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-TIME_ZONE>
|
||||
: Set the time zone here. See more details on
|
||||
why and how to set it [in the Django project documentation](https://docs.djangoproject.com/en/4.1/ref/settings/#std:setting-TIME_ZONE)
|
||||
for details on how to set it.
|
||||
|
||||
Defaults to UTC.
|
||||
@@ -757,46 +769,45 @@ should be a valid crontab(5) expression describing when to run.
|
||||
|
||||
Defaults to `30 0 * * sun` or Sunday at 30 minutes past midnight.
|
||||
|
||||
## Polling {#polling}
|
||||
`PAPERLESS_ENABLE_COMPRESSION=<bool>`
|
||||
|
||||
`PAPERLESS_CONSUMER_POLLING=<num>`
|
||||
: Enables compression of the responses from the webserver.
|
||||
|
||||
: If paperless won't find documents added to your consume folder, it
|
||||
might not be able to automatically detect filesystem changes. In
|
||||
that case, specify a polling interval in seconds here, which will
|
||||
then cause paperless to periodically check your consumption
|
||||
directory for changes. This will also disable listening for file
|
||||
system changes with `inotify`.
|
||||
: Defaults to 1, enabling compression.
|
||||
|
||||
Defaults to 0, which disables polling and uses filesystem
|
||||
notifications.
|
||||
!!! note
|
||||
|
||||
`PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=<num>`
|
||||
If you are using a proxy such as nginx, it is likely more efficient
|
||||
to enable compression in your proxy configuration rather than
|
||||
the webserver
|
||||
|
||||
: If consumer polling is enabled, sets the number of times paperless
|
||||
will check for a file to remain unmodified.
|
||||
`PAPERLESS_CONVERT_MEMORY_LIMIT=<num>`
|
||||
|
||||
Defaults to 5.
|
||||
: On smaller systems, or even in the case of Very Large Documents, the
|
||||
consumer may explode, complaining about how it's "unable to extend
|
||||
pixel cache". In such cases, try setting this to a reasonably low
|
||||
value, like 32. The default is to use whatever is necessary to do
|
||||
everything without writing to disk, and units are in megabytes.
|
||||
|
||||
`PAPERLESS_CONSUMER_POLLING_DELAY=<num>`
|
||||
For more information on how to use this value, you should search the
|
||||
web for "MAGICK_MEMORY_LIMIT".
|
||||
|
||||
: If consumer polling is enabled, sets the delay in seconds between
|
||||
each check (above) paperless will do while waiting for a file to
|
||||
remain unmodified.
|
||||
Defaults to 0, which disables the limit.
|
||||
|
||||
Defaults to 5.
|
||||
`PAPERLESS_CONVERT_TMPDIR=<path>`
|
||||
|
||||
## iNotify {#inotify}
|
||||
: Similar to the memory limit, if you've got a small system and your
|
||||
OS mounts /tmp as tmpfs, you should set this to a path that's on a
|
||||
physical disk, like /home/your_user/tmp or something. ImageMagick
|
||||
will use this as scratch space when crunching through very large
|
||||
documents.
|
||||
|
||||
`PAPERLESS_CONSUMER_INOTIFY_DELAY=<num>`
|
||||
For more information on how to use this value, you should search the
|
||||
web for "MAGICK_TMPDIR".
|
||||
|
||||
: Sets the time in seconds the consumer will wait for additional
|
||||
events from inotify before the consumer will consider a file ready
|
||||
and begin consumption. Certain scanners or network setups may
|
||||
generate multiple events for a single file, leading to multiple
|
||||
consumers working on the same file. Configure this to prevent that.
|
||||
Default is none, which disables the temporary directory.
|
||||
|
||||
Defaults to 0.5 seconds.
|
||||
## Document Consumption {#consume_config}
|
||||
|
||||
`PAPERLESS_CONSUMER_DELETE_DUPLICATES=<bool>`
|
||||
|
||||
@@ -827,96 +838,51 @@ don't exist yet.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool>`
|
||||
`PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>`
|
||||
|
||||
: Enables the scanning and page separation based on detected barcodes.
|
||||
This allows for scanning and adding multiple documents per uploaded
|
||||
file, which are separated by one or multiple barcode pages.
|
||||
: By default, paperless ignores certain files and folders in the
|
||||
consumption directory, such as system files created by the Mac OS
|
||||
or hidden folders some tools use to store data.
|
||||
|
||||
For ease of use, it is suggested to use a standardized separation
|
||||
page, e.g. [here](https://www.alliancegroup.co.uk/patch-codes.htm).
|
||||
This can be adjusted by configuring a custom json array with
|
||||
patterns to exclude.
|
||||
|
||||
If no barcodes are detected in the uploaded file, no page separation
|
||||
will happen.
|
||||
For example, `.DS_STORE/*` will ignore any files found in a folder
|
||||
named `.DS_STORE`, including `.DS_STORE/bar.pdf` and `foo/.DS_STORE/bar.pdf`
|
||||
|
||||
The original document will be removed and the separated pages will
|
||||
be saved as pdf.
|
||||
A pattern like `._*` will ignore anything starting with `._`, including:
|
||||
`._foo.pdf` and `._bar/foo.pdf`
|
||||
|
||||
Defaults to false.
|
||||
Defaults to
|
||||
`[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini", "@eaDir/*"]`.
|
||||
|
||||
`PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT=<bool>`
|
||||
`PAPERLESS_CONSUMER_BARCODE_SCANNER=<string>`
|
||||
|
||||
: Whether TIFF image files should be scanned for barcodes. This will
|
||||
automatically convert any TIFF image(s) to pdfs for later
|
||||
processing. This only has an effect, if
|
||||
PAPERLESS_CONSUMER_ENABLE_BARCODES has been enabled.
|
||||
: Sets the barcode scanner used for barcode functionality.
|
||||
|
||||
Defaults to false.
|
||||
Currently, "PYZBAR" (the default) or "ZXING" might be selected.
|
||||
If you have problems that your Barcodes/QR-Codes are not detected
|
||||
(especially with bad scan quality and/or small codes), try the other one.
|
||||
|
||||
`PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT`
|
||||
zxing is not available on all platforms.
|
||||
|
||||
: Defines the string to be detected as a separator barcode. If
|
||||
paperless is used with the PATCH-T separator pages, users shouldn't
|
||||
change this.
|
||||
`PAPERLESS_PRE_CONSUME_SCRIPT=<filename>`
|
||||
|
||||
Defaults to "PATCHT"
|
||||
: After some initial validation, Paperless can trigger an arbitrary
|
||||
script if you like before beginning consumption. This script will be provided
|
||||
data for it to work with via the environment.
|
||||
|
||||
`PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=<bool>`
|
||||
For more information, take a look at [pre-consumption script](/advanced_usage#pre-consume-script).
|
||||
|
||||
: Enables the detection of barcodes in the scanned document and
|
||||
setting the ASN (archive serial number) if a properly formatted
|
||||
barcode is detected.
|
||||
|
||||
The barcode must consist of a (configurable) prefix and the ASN
|
||||
to be set, for instance `ASN00123`.
|
||||
|
||||
This option is compatible with barcode page separation, since
|
||||
pages will be split up before reading the ASN.
|
||||
|
||||
If no ASN barcodes are detected in the uploaded file, no ASN will
|
||||
be set. If a barcode with an already existing ASN is detected, no ASN
|
||||
will be set either and a warning will be logged.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX=ASN`
|
||||
|
||||
: Defines the prefix that is used to identify a barcode as an ASN
|
||||
barcode.
|
||||
|
||||
Defaults to "ASN"
|
||||
|
||||
`PAPERLESS_CONVERT_MEMORY_LIMIT=<num>`
|
||||
|
||||
: On smaller systems, or even in the case of Very Large Documents, the
|
||||
consumer may explode, complaining about how it's "unable to extend
|
||||
pixel cache". In such cases, try setting this to a reasonably low
|
||||
value, like 32. The default is to use whatever is necessary to do
|
||||
everything without writing to disk, and units are in megabytes.
|
||||
|
||||
For more information on how to use this value, you should search the
|
||||
web for "MAGICK_MEMORY_LIMIT".
|
||||
|
||||
Defaults to 0, which disables the limit.
|
||||
|
||||
`PAPERLESS_CONVERT_TMPDIR=<path>`
|
||||
|
||||
: Similar to the memory limit, if you've got a small system and your
|
||||
OS mounts /tmp as tmpfs, you should set this to a path that's on a
|
||||
physical disk, like /home/your_user/tmp or something. ImageMagick
|
||||
will use this as scratch space when crunching through very large
|
||||
documents.
|
||||
|
||||
For more information on how to use this value, you should search the
|
||||
web for "MAGICK_TMPDIR".
|
||||
|
||||
Default is none, which disables the temporary directory.
|
||||
The default is blank, which means nothing will be executed.
|
||||
|
||||
`PAPERLESS_POST_CONSUME_SCRIPT=<filename>`
|
||||
|
||||
: After a document is consumed, Paperless can trigger an arbitrary
|
||||
script if you like. This script will be passed a number of arguments
|
||||
for you to work with. For more information, take a look at [Post-consumption script](/advanced_usage#post-consume-script).
|
||||
script if you like. This script will be provided
|
||||
data for it to work with via the environment.
|
||||
|
||||
For more information, take a look at [Post-consumption script](/advanced_usage#post-consume-script).
|
||||
|
||||
The default is blank, which means nothing will be executed.
|
||||
|
||||
@@ -983,16 +949,109 @@ within your documents.
|
||||
second, and year last order. Characters D, M, or Y can be shuffled
|
||||
to meet the required order.
|
||||
|
||||
`PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>`
|
||||
### Polling {#polling}
|
||||
|
||||
: By default, paperless ignores certain files and folders in the
|
||||
consumption directory, such as system files created by the Mac OS.
|
||||
`PAPERLESS_CONSUMER_POLLING=<num>`
|
||||
|
||||
This can be adjusted by configuring a custom json array with
|
||||
patterns to exclude.
|
||||
: If paperless won't find documents added to your consume folder, it
|
||||
might not be able to automatically detect filesystem changes. In
|
||||
that case, specify a polling interval in seconds here, which will
|
||||
then cause paperless to periodically check your consumption
|
||||
directory for changes. This will also disable listening for file
|
||||
system changes with `inotify`.
|
||||
|
||||
Defaults to
|
||||
`[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]`.
|
||||
Defaults to 0, which disables polling and uses filesystem
|
||||
notifications.
|
||||
|
||||
`PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=<num>`
|
||||
|
||||
: If consumer polling is enabled, sets the number of times paperless
|
||||
will check for a file to remain unmodified.
|
||||
|
||||
Defaults to 5.
|
||||
|
||||
`PAPERLESS_CONSUMER_POLLING_DELAY=<num>`
|
||||
|
||||
: If consumer polling is enabled, sets the delay in seconds between
|
||||
each check (above) paperless will do while waiting for a file to
|
||||
remain unmodified.
|
||||
|
||||
Defaults to 5.
|
||||
|
||||
### iNotify {#inotify}
|
||||
|
||||
`PAPERLESS_CONSUMER_INOTIFY_DELAY=<num>`
|
||||
|
||||
: Sets the time in seconds the consumer will wait for additional
|
||||
events from inotify before the consumer will consider a file ready
|
||||
and begin consumption. Certain scanners or network setups may
|
||||
generate multiple events for a single file, leading to multiple
|
||||
consumers working on the same file. Configure this to prevent that.
|
||||
|
||||
Defaults to 0.5 seconds.
|
||||
|
||||
## Barcodes {#barcodes}
|
||||
|
||||
`PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool>`
|
||||
|
||||
: Enables the scanning and page separation based on detected barcodes.
|
||||
This allows for scanning and adding multiple documents per uploaded
|
||||
file, which are separated by one or multiple barcode pages.
|
||||
|
||||
For ease of use, it is suggested to use a standardized separation
|
||||
page, e.g. [here](https://www.alliancegroup.co.uk/patch-codes.htm).
|
||||
|
||||
If no barcodes are detected in the uploaded file, no page separation
|
||||
will happen.
|
||||
|
||||
The original document will be removed and the separated pages will
|
||||
be saved as pdf.
|
||||
|
||||
See additional information in the [advanced usage documentation](/advanced_usage#barcodes)
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT=<bool>`
|
||||
|
||||
: Whether TIFF image files should be scanned for barcodes. This will
|
||||
automatically convert any TIFF image(s) to pdfs for later
|
||||
processing. This only has an effect, if
|
||||
PAPERLESS_CONSUMER_ENABLE_BARCODES has been enabled.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_CONSUMER_BARCODE_STRING=<string>`
|
||||
|
||||
: Defines the string to be detected as a separator barcode. If
|
||||
paperless is used with the PATCH-T separator pages, users shouldn't
|
||||
change this.
|
||||
|
||||
Defaults to "PATCHT"
|
||||
|
||||
`PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=<bool>`
|
||||
|
||||
: Enables the detection of barcodes in the scanned document and
|
||||
setting the ASN (archive serial number) if a properly formatted
|
||||
barcode is detected.
|
||||
|
||||
The barcode must consist of a (configurable) prefix and the ASN
|
||||
to be set, for instance `ASN00123`.
|
||||
|
||||
This option is compatible with barcode page separation, since
|
||||
pages will be split up before reading the ASN.
|
||||
|
||||
If no ASN barcodes are detected in the uploaded file, no ASN will
|
||||
be set. If a barcode with an already existing ASN is detected, no ASN
|
||||
will be set either and a warning will be logged.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX=<string>`
|
||||
|
||||
: Defines the prefix that is used to identify a barcode as an ASN
|
||||
barcode.
|
||||
|
||||
Defaults to "ASN"
|
||||
|
||||
## Binaries
|
||||
|
||||
@@ -1084,12 +1143,14 @@ actual group ID on the host system, which you can get by executing
|
||||
: Additional OCR languages to install. By default, paperless comes
|
||||
with English, German, Italian, Spanish and French. If your language
|
||||
is not in this list, install additional languages with this
|
||||
configuration option:
|
||||
configuration option ([find the right LangCodes](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html)):
|
||||
|
||||
``` bash
|
||||
PAPERLESS_OCR_LANGUAGES=tur ces
|
||||
```
|
||||
|
||||
Make sure it's a space separated list when using several values.
|
||||
|
||||
To actually use these languages, also set the default OCR language
|
||||
of paperless:
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
# Development
|
||||
|
||||
This section describes the steps you need to take to start development
|
||||
on paperless-ngx.
|
||||
on Paperless-ngx.
|
||||
|
||||
Check out the source from github. The repository is organized in the
|
||||
Check out the source from GitHub. The repository is organized in the
|
||||
following way:
|
||||
|
||||
- `main` always represents the latest release and will only see
|
||||
@@ -12,7 +12,7 @@ following way:
|
||||
- `feature-X` contain bigger changes that will be in some release, but
|
||||
not necessarily the next one.
|
||||
|
||||
When making functional changes to paperless, _always_ make your changes
|
||||
When making functional changes to Paperless-ngx, _always_ make your changes
|
||||
on the `dev` branch.
|
||||
|
||||
Apart from that, the folder structure is as follows:
|
||||
@@ -24,9 +24,9 @@ Apart from that, the folder structure is as follows:
|
||||
development.
|
||||
- `docker/` - Files required to build the docker image.
|
||||
|
||||
## Contributing to Paperless
|
||||
## Contributing to Paperless-ngx
|
||||
|
||||
Maybe you've been using Paperless for a while and want to add a feature
|
||||
Maybe you've been using Paperless-ngx 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!
|
||||
@@ -36,13 +36,13 @@ conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUC
|
||||
and other important information in the [contributing
|
||||
guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## 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`](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
|
||||
hook to perform some formatting and linting before a commit is allowed.
|
||||
the project utilizes Git [`pre-commit`](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
|
||||
hooks 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.
|
||||
early on.
|
||||
|
||||
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.
|
||||
@@ -51,129 +51,115 @@ 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
|
||||
## General setup
|
||||
|
||||
After you forked and cloned the code from github you need to perform a
|
||||
first-time setup. To do the setup you need to perform the steps from the
|
||||
following chapters in a certain order:
|
||||
After you forked and cloned the code from GitHub you need to perform a
|
||||
first-time setup.
|
||||
|
||||
!!! note
|
||||
|
||||
Every command is executed directly from the root folder of the project unless specified otherwise.
|
||||
|
||||
1. Install prerequisites + pipenv as mentioned in
|
||||
[Bare metal route](/setup#bare_metal)
|
||||
[Bare metal route](/setup#bare_metal).
|
||||
|
||||
2. Copy `paperless.conf.example` to `paperless.conf` and enable debug
|
||||
mode.
|
||||
mode within the file via `PAPERLESS_DEBUG=true`.
|
||||
|
||||
3. Install the Angular CLI interface:
|
||||
3. Create `consume` and `media` directories:
|
||||
|
||||
```shell-session
|
||||
$ npm install -g @angular/cli
|
||||
```bash
|
||||
$ mkdir -p consume media
|
||||
```
|
||||
|
||||
4. Install pre-commit hooks
|
||||
4. Install the Python dependencies:
|
||||
|
||||
```shell-session
|
||||
pre-commit install
|
||||
```bash
|
||||
$ pipenv install --dev
|
||||
```
|
||||
|
||||
5. Create `consume` and `media` folders in the cloned root folder.
|
||||
!!! note
|
||||
|
||||
```shell-session
|
||||
mkdir -p consume media
|
||||
Using a virtual environment is highly recommended. You can spawn one via `pipenv shell`.
|
||||
Make sure you're using Python 3.10.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.
|
||||
|
||||
5. Install pre-commit hooks:
|
||||
|
||||
```bash
|
||||
$ pre-commit install
|
||||
```
|
||||
|
||||
6. You can now either ...
|
||||
6. Apply migrations and create a superuser for your development instance:
|
||||
|
||||
```bash
|
||||
# src/
|
||||
|
||||
$ python3 manage.py migrate
|
||||
$ python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
7. You can now either ...
|
||||
|
||||
- install redis or
|
||||
|
||||
- use the included scripts/start-services.sh to use docker to fire
|
||||
- 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 database server) or
|
||||
|
||||
- spin up a bare redis container
|
||||
|
||||
```shell-session
|
||||
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
||||
```
|
||||
$ docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
||||
```
|
||||
|
||||
7. Install the python dependencies by performing in the src/ directory.
|
||||
|
||||
```shell-session
|
||||
pipenv install --dev
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Make sure you're using python 3.10.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.
|
||||
|
||||
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:
|
||||
|
||||
```shell-session
|
||||
npm install .
|
||||
./node_modules/.bin/ng build --configuration production
|
||||
```
|
||||
|
||||
9. Apply migrations and create a superuser for your dev instance:
|
||||
|
||||
```shell-session
|
||||
python3 manage.py migrate
|
||||
python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```shell-session
|
||||
python3 manage.py runserver & python3 manage.py document_consumer & celery --app paperless worker
|
||||
```
|
||||
|
||||
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.
|
||||
8. Continue with either back-end or front-end development – or both :-).
|
||||
|
||||
## Back end development
|
||||
|
||||
The backend is a [Django](https://www.djangoproject.com/) application. PyCharm works well for development,
|
||||
but you can use whatever you want.
|
||||
The back end is a [Django](https://www.djangoproject.com/) application.
|
||||
[PyCharm](https://www.jetbrains.com/de-de/pycharm/) as well as [Visual Studio Code](https://code.visualstudio.com)
|
||||
work well for development, but you can use whatever you want.
|
||||
|
||||
Configure the IDE to use the src/ folder as the base source folder.
|
||||
Configure the IDE to use the `src/`-folder as the base source folder.
|
||||
Configure the following launch configurations in your IDE:
|
||||
|
||||
- `python3 manage.py runserver`
|
||||
- `celery --app paperless worker`
|
||||
- `python3 manage.py document_consumer`
|
||||
- `celery --app paperless worker -l DEBUG` (or any other log level)
|
||||
|
||||
To start them all:
|
||||
|
||||
```shell-session
|
||||
python3 manage.py runserver & python3 manage.py document_consumer & celery --app paperless worker
|
||||
```bash
|
||||
# src/
|
||||
|
||||
$ python3 manage.py runserver & \
|
||||
python3 manage.py document_consumer & \
|
||||
celery --app paperless worker -l DEBUG
|
||||
```
|
||||
|
||||
Testing and code style:
|
||||
You might need the front end to test your back end code.
|
||||
This assumes that you have AngularJS installed on your system.
|
||||
Go to the [Front end development](#front-end-development) section for further details.
|
||||
To build the front end once use this command:
|
||||
|
||||
```bash
|
||||
# src-ui/
|
||||
|
||||
$ npm install
|
||||
$ ng build --configuration production
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
- 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
|
||||
generates a HTML coverage 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.
|
||||
|
||||
- Coding style is enforced by the Git pre-commit hooks. These will
|
||||
ensure your code is formatted and do some linting when you do a `git commit`.
|
||||
|
||||
- You can also run `black` manually to format your code
|
||||
|
||||
- The `pre-commit` hooks will modify files and interact with each other.
|
||||
It may take a couple of `git add`, `git commit` cycle to satisfy them.
|
||||
|
||||
!!! note
|
||||
|
||||
The line length rule E501 is generally useful for getting multiple
|
||||
@@ -184,98 +170,98 @@ Testing and code style:
|
||||
|
||||
## Front end development
|
||||
|
||||
The front end is built using Angular. In order to get started, you need
|
||||
`npm`. Install the Angular CLI interface with
|
||||
The front end is built using AngularJS. In order to get started, you need Node.js (version 14.15+) and
|
||||
`npm`.
|
||||
|
||||
```shell-session
|
||||
$ npm install -g @angular/cli
|
||||
```
|
||||
!!! note
|
||||
|
||||
and make sure that it's on your path. Next, in the src-ui/ directory,
|
||||
install the required dependencies of the project.
|
||||
The following commands are all performed in the `src-ui`-directory. You will need a running back end (including an active session) to connect to the back end API. To spin it up refer to the commands under the section [above](#back-end-development).
|
||||
|
||||
```shell-session
|
||||
$ npm install
|
||||
```
|
||||
1. Install the Angular CLI. You might need sudo privileges
|
||||
to perform this command:
|
||||
|
||||
You can launch a development server by running
|
||||
```bash
|
||||
$ npm install -g @angular/cli
|
||||
```
|
||||
|
||||
```shell-session
|
||||
$ ng serve
|
||||
```
|
||||
2. Make sure that it's on your path.
|
||||
|
||||
This will automatically update whenever you save. However, in-place
|
||||
compilation might fail on syntax errors, in which case you need to
|
||||
restart it.
|
||||
3. Install all neccessary modules:
|
||||
|
||||
By default, the development server is available on
|
||||
`http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If
|
||||
you enabled DEBUG on the back end, several security overrides for
|
||||
allowed hosts, CORS and X-Frame-Options are in place so that the front
|
||||
end behaves exactly as in production. This also relies on you being
|
||||
logged into the back end. Without a valid session, The front end will
|
||||
simply not work.
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
Testing and code style:
|
||||
4. You can launch a development server by running:
|
||||
|
||||
- The frontend code (.ts, .html, .scss) use `prettier` for code
|
||||
```bash
|
||||
$ ng serve
|
||||
```
|
||||
|
||||
This will automatically update whenever you save. However, in-place
|
||||
compilation might fail on syntax errors, in which case you need to
|
||||
restart it.
|
||||
|
||||
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
|
||||
|
||||
### Testing and code style
|
||||
|
||||
- The front end code (.ts, .html, .scss) use `prettier` for code
|
||||
formatting via the Git `pre-commit` hooks which run automatically on
|
||||
commit. See
|
||||
[above](#code-formatting-with-pre-commit-hooks) for installation. You can also run this via cli with a
|
||||
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
|
||||
command such as
|
||||
|
||||
```shell-session
|
||||
```bash
|
||||
$ 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,
|
||||
- Front end testing uses jest and cypress. There is currently a need
|
||||
for significantly more front end tests. Unit tests and e2e tests,
|
||||
respectively, can be run non-interactively with:
|
||||
|
||||
```shell-session
|
||||
```bash
|
||||
$ ng test
|
||||
$ npm run e2e:ci
|
||||
```
|
||||
|
||||
Cypress also includes a UI which can be run from within the `src-ui`
|
||||
directory with
|
||||
- Cypress also includes a UI which can be run with:
|
||||
|
||||
```shell-session
|
||||
$ ./node_modules/.bin/cypress open
|
||||
```bash
|
||||
$ ./node_modules/.bin/cypress open
|
||||
```
|
||||
|
||||
- In order to build the front end and serve it as part of Django, execute:
|
||||
|
||||
```bash
|
||||
$ ng build --configuration production
|
||||
```
|
||||
|
||||
In order to build the front end and serve it as part of django, execute
|
||||
|
||||
```shell-session
|
||||
$ ng build --configuration production
|
||||
```
|
||||
|
||||
This will build the front end and put it in a location from which the
|
||||
Django server will serve it as static content. This way, you can verify
|
||||
that authentication is working.
|
||||
This will build the front end and put it in a location from which the
|
||||
Django server will serve it as static content. This way, you can verify
|
||||
that authentication is working.
|
||||
|
||||
## Localization
|
||||
|
||||
Paperless is available in many different languages. Since paperless
|
||||
consists both of a django application and an Angular front end, both
|
||||
Paperless-ngx is available in many different languages. Since Paperless-ngx
|
||||
consists both of a Django application and an AngularJS front end, both
|
||||
these parts have to be translated separately.
|
||||
|
||||
### Front end localization
|
||||
|
||||
- The Angular front end does localization according to the [Angular
|
||||
- The AngularJS front end does localization according to the [Angular
|
||||
documentation](https://angular.io/guide/i18n).
|
||||
- The source language of the project is "en_US".
|
||||
- The source strings end up in the file "src-ui/messages.xlf".
|
||||
- The source strings end up in the file `src-ui/messages.xlf`.
|
||||
- The translated strings need to be placed in the
|
||||
"src-ui/src/locale/" folder.
|
||||
`src-ui/src/locale/` folder.
|
||||
- In order to extract added or changed strings from the source files,
|
||||
call `ng xi18n --ivy`.
|
||||
call `ng extract-i18n`.
|
||||
|
||||
Adding new languages requires adding the translated files in the
|
||||
"src-ui/src/locale/" folder and adjusting a couple files.
|
||||
`src-ui/src/locale/` folder and adjusting a couple files.
|
||||
|
||||
1. Adjust "src-ui/angular.json":
|
||||
1. Adjust `src-ui/angular.json`:
|
||||
|
||||
```json
|
||||
"i18n": {
|
||||
@@ -292,7 +278,7 @@ Adding new languages requires adding the translated files in the
|
||||
```
|
||||
|
||||
2. Add the language to the available options in
|
||||
"src-ui/src/app/services/settings.service.ts":
|
||||
`src-ui/src/app/services/settings.service.ts`:
|
||||
|
||||
```typescript
|
||||
getLanguageOptions(): LanguageOption[] {
|
||||
@@ -313,7 +299,7 @@ Adding new languages requires adding the translated files in the
|
||||
and "yyyy".
|
||||
|
||||
3. Import and register the Angular data for this locale in
|
||||
"src-ui/src/app/app.module.ts":
|
||||
`src-ui/src/app/app.module.ts`:
|
||||
|
||||
```typescript
|
||||
import localeDe from '@angular/common/locales/de'
|
||||
@@ -326,10 +312,10 @@ A majority of the strings that appear in the back end appear only when
|
||||
the admin is used. However, some of these are still shown on the front
|
||||
end (such as error messages).
|
||||
|
||||
- The django application does localization according to the [django
|
||||
- The django application does localization according to the [Django
|
||||
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
|
||||
- The source language of the project is "en_US".
|
||||
- Localization files end up in the folder "src/locale/".
|
||||
- Localization files end up in the folder `src/locale/`.
|
||||
- In order to extract strings from the application, call
|
||||
`python3 manage.py makemessages -l en_US`. This is important after
|
||||
making changes to translatable strings.
|
||||
@@ -340,8 +326,8 @@ end (such as error messages).
|
||||
command.
|
||||
|
||||
Adding new languages requires adding the translated files in the
|
||||
"src/locale/" folder and adjusting the file
|
||||
"src/paperless/settings.py" to include the new language:
|
||||
`src/locale/`-folder and adjusting the file
|
||||
`src/paperless/settings.py` to include the new language:
|
||||
|
||||
```python
|
||||
LANGUAGES = [
|
||||
@@ -360,18 +346,27 @@ LANGUAGES = [
|
||||
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
||||
If you want to build the documentation locally, this is how you do it:
|
||||
|
||||
1. Install python dependencies.
|
||||
1. Have an active pipenv shell (`pipenv shell`) and install Python dependencies:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
```bash
|
||||
$ pipenv install --dev
|
||||
```
|
||||
|
||||
2. Build the documentation
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ pipenv mkdocs build --config-file mkdocs.yml
|
||||
```bash
|
||||
$ mkdocs build --config-file mkdocs.yml
|
||||
```
|
||||
|
||||
_alternatively..._
|
||||
|
||||
3. Serve the documentation. This will spin up a
|
||||
copy of the documentation at http://127.0.0.1:8000
|
||||
that will automatically refresh everytime you change
|
||||
something.
|
||||
|
||||
```bash
|
||||
$ mkdocs serve
|
||||
```
|
||||
|
||||
## Building the Docker image
|
||||
@@ -384,35 +379,35 @@ helper script `build-docker-image.sh`.
|
||||
|
||||
Building the docker image from source:
|
||||
|
||||
```shell-session
|
||||
```bash
|
||||
./build-docker-image.sh Dockerfile -t <your-tag>
|
||||
```
|
||||
|
||||
## Extending Paperless
|
||||
## Extending Paperless-ngx
|
||||
|
||||
Paperless does not have any fancy plugin systems and will probably never
|
||||
Paperless-ngx does not have any fancy plugin systems and will probably never
|
||||
have. However, some parts of the application have been designed to allow
|
||||
easy integration of additional features without any modification to the
|
||||
base code.
|
||||
|
||||
### Making custom parsers
|
||||
|
||||
Paperless uses parsers to add documents to paperless. A parser is
|
||||
Paperless-ngx uses parsers to add documents. A parser is
|
||||
responsible for:
|
||||
|
||||
- Retrieve the content from the original
|
||||
- Create a thumbnail
|
||||
- Optional: Retrieve a created date from the original
|
||||
- Optional: Create an archived document from the original
|
||||
- Retrieving the content from the original
|
||||
- Creating a thumbnail
|
||||
- _optional:_ Retrieving a created date from the original
|
||||
- _optional:_ Creainge an archived document from the original
|
||||
|
||||
Custom parsers can be added to paperless to support more file types. In
|
||||
Custom parsers can be added to Paperless-ngx to support more file types. In
|
||||
order to do that, you need to write the parser itself and announce its
|
||||
existence to paperless.
|
||||
existence to Paperless-ngx.
|
||||
|
||||
The parser itself must extend `documents.parsers.DocumentParser` and
|
||||
must implement the methods `parse` and `get_thumbnail`. You can provide
|
||||
your own implementation to `get_date` if you don't want to rely on
|
||||
paperless' default date guessing mechanisms.
|
||||
Paperless-ngx' default date guessing mechanisms.
|
||||
|
||||
```python
|
||||
class MyCustomParser(DocumentParser):
|
||||
@@ -444,7 +439,7 @@ to be empty and removed after consumption finished. You can use that
|
||||
directory to store any intermediate files and also use it to store the
|
||||
thumbnail / archived document.
|
||||
|
||||
After that, you need to announce your parser to paperless. You need to
|
||||
After that, you need to announce your parser to Paperless-ngx. You need to
|
||||
connect a handler to the `document_consumer_declaration` signal. Have a
|
||||
look in the file `src/paperless_tesseract/apps.py` on how that's done.
|
||||
The handler is a method that returns information about your parser:
|
||||
@@ -464,11 +459,11 @@ def myparser_consumer_declaration(sender, **kwargs):
|
||||
- `parser` is a reference to a class that extends `DocumentParser`.
|
||||
- `weight` is used whenever two or more parsers are able to parse a
|
||||
file: The parser with the higher weight wins. This can be used to
|
||||
override the parsers provided by paperless.
|
||||
override the parsers provided by Paperless-ngx.
|
||||
- `mime_types` is a dictionary. The keys are the mime types your
|
||||
parser supports and the value is the default file extension that
|
||||
paperless should use when storing files and serving them for
|
||||
Paperless-ngx should use when storing files and serving them for
|
||||
download. We could guess that from the file extensions, but some
|
||||
mime types have many extensions associated with them and the python
|
||||
mime types have many extensions associated with them and the Python
|
||||
methods responsible for guessing the extension do not always return
|
||||
the same value.
|
||||
|
@@ -43,7 +43,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
```
|
||||
|
||||
2. Go to the [/docker/compose directory on the project
|
||||
page](https://github.com/paperless-ngx/paperless-ngx/tree/master/docker/compose)
|
||||
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||
and download one of the `docker-compose.*.yml` files,
|
||||
depending on which database backend you want to use. Rename this
|
||||
file to `docker-compose.yml`. If you want to enable
|
||||
@@ -160,8 +160,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
`PAPERLESS_CONSUMER_POLLING`, which will disable inotify. See
|
||||
[here](/configuration#polling).
|
||||
|
||||
6. Run `docker-compose pull`, followed by `docker-compose up -d`. This
|
||||
will pull the image, create and start the necessary containers.
|
||||
6. Run `docker-compose pull`. This will pull the image.
|
||||
|
||||
7. To be able to login, you will need a super user. To create it,
|
||||
execute the following command:
|
||||
@@ -173,7 +172,9 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
This will prompt you to set a username, an optional e-mail address
|
||||
and finally a password (at least 8 characters).
|
||||
|
||||
8. The default `docker-compose.yml` exports the webserver on your local
|
||||
8. Run `docker-compose up -d`. This will create and start the necessary containers.
|
||||
|
||||
9. The default `docker-compose.yml` exports the webserver on your local
|
||||
port
|
||||
|
||||
8000\. If you did not change this, you should now be able to visit
|
||||
@@ -189,7 +190,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
git clone https://github.com/paperless-ngx/paperless-ngx
|
||||
```
|
||||
|
||||
The master branch always reflects the latest stable version.
|
||||
The main branch always reflects the latest stable version.
|
||||
|
||||
2. Copy one of the `docker/compose/docker-compose.*.yml` to
|
||||
`docker-compose.yml` in the root folder, depending on which database
|
||||
@@ -365,6 +366,10 @@ supported.
|
||||
documents are written in.
|
||||
- Set `PAPERLESS_TIME_ZONE` to your local time zone.
|
||||
|
||||
!!! warning
|
||||
|
||||
Ensure your Redis instance [is secured](https://redis.io/docs/getting-started/#securing-redis).
|
||||
|
||||
7. Create the following directories if they are missing:
|
||||
|
||||
- `/opt/paperless/media`
|
||||
@@ -388,12 +393,7 @@ supported.
|
||||
```
|
||||
|
||||
8. Install python requirements from the `requirements.txt` file. It is
|
||||
up to you if you wish to use a virtual environment or not. First you
|
||||
should update your pip, so it gets the actual packages.
|
||||
|
||||
```shell-session
|
||||
sudo -Hu paperless pip3 install --upgrade pip
|
||||
```
|
||||
up to you if you wish to use a virtual environment or not. First you should update your pip, so it gets the actual packages.
|
||||
|
||||
```shell-session
|
||||
sudo -Hu paperless pip3 install -r requirements.txt
|
||||
@@ -590,7 +590,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
||||
|
||||
3. Download the latest release of paperless-ngx. You can either go with
|
||||
the docker-compose files from
|
||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/master/docker/compose)
|
||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||
or clone the repository to build the image yourself (see
|
||||
[above](#docker_build)). You can
|
||||
either replace your current paperless folder or put paperless-ngx in
|
||||
@@ -713,6 +713,12 @@ below use PostgreSQL, but are applicable to MySQL/MariaDB with the
|
||||
MySQL also enforces limits on maximum lengths, but does so differently than
|
||||
PostgreSQL. It may not be possible to migrate to MySQL due to this.
|
||||
|
||||
!!! warning
|
||||
|
||||
Using mariadb version 10.4+ is recommended. Using the `utf8mb3` character set on
|
||||
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||
|
||||
1. Stop paperless, if it is running.
|
||||
|
||||
2. Tell paperless to use PostgreSQL:
|
||||
@@ -817,14 +823,14 @@ performance immensely:
|
||||
other tasks).
|
||||
- Keep `PAPERLESS_OCR_MODE` at its default value `skip` and consider
|
||||
OCR'ing your documents before feeding them into paperless. Some
|
||||
scanners are able to do this! You might want to even specify
|
||||
`skip_noarchive` to skip archive file generation for already ocr'ed
|
||||
documents entirely.
|
||||
scanners are able to do this!
|
||||
- Set `PAPERLESS_OCR_SKIP_ARCHIVE_FILE` to `with_text` to skip archive
|
||||
file generation for already ocr'ed documents, or `always` to skip it
|
||||
for all documents.
|
||||
- If you want to perform OCR on the device, consider using
|
||||
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
||||
less memory at the expense of slightly worse OCR results.
|
||||
- If using docker, consider setting `PAPERLESS_WEBSERVER_WORKERS` to
|
||||
1. This will save some memory.
|
||||
- If using docker, consider setting `PAPERLESS_WEBSERVER_WORKERS` to 1. This will save some memory.
|
||||
- Consider setting `PAPERLESS_ENABLE_NLTK` to false, to disable the
|
||||
more advanced language processing, which can take more memory and
|
||||
processing time.
|
||||
|
@@ -332,3 +332,16 @@ change the port gunicorn listens on.
|
||||
|
||||
To fix this, set `PAPERLESS_PORT` again to your desired port, or the
|
||||
default of 8000.
|
||||
|
||||
## Database Warns about unique constraint "documents_tag_name_uniq
|
||||
|
||||
You may see database log lines like:
|
||||
|
||||
```
|
||||
ERROR: duplicate key value violates unique constraint "documents_tag_name_uniq"
|
||||
DETAIL: Key (name)=(NameF) already exists.
|
||||
STATEMENT: INSERT INTO "documents_tag" ("owner_id", "name", "match", "matching_algorithm", "is_insensitive", "color", "is_inbox_tag") VALUES (NULL, 'NameF', '', 1, true, '#a6cee3', false) RETURNING "documents_tag"."id"
|
||||
```
|
||||
|
||||
This can happen during heavy consumption when using polling. Paperless will handle it correctly and the file
|
||||
will still be consumed
|
||||
|
@@ -60,8 +60,8 @@ following operations on your documents:
|
||||
|
||||
This process can be configured to fit your needs. If you don't want
|
||||
paperless to create archived versions for digital documents, you can
|
||||
configure that by configuring `PAPERLESS_OCR_MODE=skip_noarchive`.
|
||||
Please read the
|
||||
configure that by configuring
|
||||
`PAPERLESS_OCR_SKIP_ARCHIVE_FILE=with_text`. Please read the
|
||||
[relevant section in the documentation](/configuration#ocr).
|
||||
|
||||
!!! note
|
||||
@@ -202,6 +202,39 @@ configured via `PAPERLESS_EMAIL_TASK_CRON` (see [software tweaks](/configuration
|
||||
You can also submit a document using the REST API, see [POSTing documents](/api#file-uploads)
|
||||
for details.
|
||||
|
||||
## Permissions
|
||||
|
||||
As of version 1.13.0 Paperless-ngx added core support for user / group permissions. Permissions is
|
||||
based around an object 'owner' and 'view' and 'edit' permissions can be granted to other users
|
||||
or groups.
|
||||
|
||||
Permissions uses the built-in user model of the backend framework, Django.
|
||||
|
||||
!!! note
|
||||
|
||||
After migration to version 1.13.0 all existing documents, tags etc. will have no explicit owner
|
||||
set which means they will be visible / editable by all users. Once an object has an owner set,
|
||||
only the owner can explicitly grant / revoke permissions.
|
||||
|
||||
!!! note
|
||||
|
||||
When first migrating to permissions it is recommended to user a 'superuser' account (which
|
||||
would usually have been setup during installation) to ensure you have full permissions.
|
||||
|
||||
Note that superusers have access to all objects.
|
||||
|
||||
Permissions can be set using the new "Permissions" tab when editing documents, or bulk-applied
|
||||
in the UI by selecting documents and choosing the "Permissions" button. Owner can also optionally
|
||||
be set for documents uploaded via the API. Documents consumed via the consumption dir currently
|
||||
do not have an owner set.
|
||||
|
||||
### Users and Groups
|
||||
|
||||
Paperless-ngx versions after 1.13.0 allow creating and editing users and groups via the 'frontend' UI.
|
||||
These can be found under Settings > Users & Groups, assuming the user has access. If a user is designated
|
||||
as a member of a group those permissions will be inherited and this is reflected in the UI. Explicit
|
||||
permissions can be granted to limit access to certain parts of the UI (and corresponding API endpoints).
|
||||
|
||||
## Best practices {#basic-searching}
|
||||
|
||||
Paperless offers a couple tools that help you organize your document
|
||||
|
@@ -30,7 +30,9 @@ def worker_int(worker):
|
||||
worker.log.info("worker received INT or QUIT signal")
|
||||
|
||||
## get traceback info
|
||||
import threading, sys, traceback
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
id2name = {th.ident: th.name for th in threading.enumerate()}
|
||||
code = []
|
||||
|
@@ -321,7 +321,7 @@ fi
|
||||
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/main/docker/compose/.env" -O .env
|
||||
|
||||
SECRET_KEY=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1)
|
||||
SECRET_KEY=$(tr --delete --complement 'a-zA-Z0-9' < /dev/urandom 2>/dev/null | head --bytes 64)
|
||||
|
||||
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
||||
|
||||
@@ -346,7 +346,7 @@ read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
|
||||
fi
|
||||
} > docker-compose.env
|
||||
|
||||
sed -i "s/- 8000:8000/- $PORT:8000/g" docker-compose.yml
|
||||
sed -i "s/- \"8000:8000\"/- \"$PORT:8000\"/g" docker-compose.yml
|
||||
|
||||
sed -i "s#- \./consume:/usr/src/paperless/consume#- $CONSUME_FOLDER:/usr/src/paperless/consume#g" docker-compose.yml
|
||||
|
||||
|
@@ -41,6 +41,7 @@ markdown_extensions:
|
||||
anchor_linenums: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
strict: true
|
||||
nav:
|
||||
- index.md
|
||||
@@ -54,7 +55,7 @@ nav:
|
||||
- 'FAQs': faq.md
|
||||
- troubleshooting.md
|
||||
- changelog.md
|
||||
copyright: Copyright © 2016 - 2022 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
|
||||
copyright: Copyright © 2016 - 2023 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
|
||||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
|
@@ -42,6 +42,7 @@
|
||||
|
||||
#PAPERLESS_OCR_LANGUAGE=eng
|
||||
#PAPERLESS_OCR_MODE=skip
|
||||
#PAPERLESS_OCR_SKIP_ARCHIVE_FILE=never
|
||||
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
|
||||
#PAPERLESS_OCR_PAGES=1
|
||||
#PAPERLESS_OCR_IMAGE_DPI=300
|
||||
|
@@ -2,5 +2,5 @@
|
||||
|
||||
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 -p 3000:3000 -d gotenberg/gotenberg:7.6 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
||||
|
@@ -192,7 +192,8 @@
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
],
|
||||
"analytics": false
|
||||
},
|
||||
"schematics": {
|
||||
"@angular-eslint/schematics:application": {
|
||||
|
@@ -2,6 +2,7 @@ import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
videosFolder: 'cypress/videos',
|
||||
video: false,
|
||||
screenshotsFolder: 'cypress/screenshots',
|
||||
fixturesFolder: 'cypress/fixtures',
|
||||
e2e: {
|
||||
|
68
src-ui/cypress/e2e/auth/auth.cy.ts
Normal file
68
src-ui/cypress/e2e/auth/auth.cy.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
describe('settings', () => {
|
||||
beforeEach(() => {
|
||||
// also uses global fixtures from cypress/support/e2e.ts
|
||||
|
||||
// mock restricted permissions
|
||||
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||
fixture: 'ui_settings/settings_restricted.json',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not allow user to edit settings', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.visit('/settings').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view documents', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Documents').should('not.exist')
|
||||
cy.visit('/documents').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
cy.visit('/documents/1').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view correspondents', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Correspondents').should('not.exist')
|
||||
cy.visit('/correspondents').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view tags', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Tags').should('not.exist')
|
||||
cy.visit('/tags').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view document types', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Document Types').should('not.exist')
|
||||
cy.visit('/documenttypes').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view storage paths', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Storage Paths').should('not.exist')
|
||||
cy.visit('/storagepaths').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view logs', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Logs').should('not.exist')
|
||||
cy.visit('/logs').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
|
||||
it('should not allow user to view tasks', () => {
|
||||
cy.visit('/dashboard')
|
||||
cy.contains('Tasks').should('not.exist')
|
||||
cy.visit('/tasks').wait(2000)
|
||||
cy.contains("You don't have permissions to do that").should('exist')
|
||||
})
|
||||
})
|
@@ -17,28 +17,28 @@ describe('document-detail', () => {
|
||||
req.reply({ result: 'OK' })
|
||||
}).as('saveDoc')
|
||||
|
||||
cy.fixture('documents/1/comments.json').then((commentsJson) => {
|
||||
cy.fixture('documents/1/notes.json').then((notesJson) => {
|
||||
cy.intercept(
|
||||
'GET',
|
||||
'http://localhost:8000/api/documents/1/comments/',
|
||||
'http://localhost:8000/api/documents/1/notes/',
|
||||
(req) => {
|
||||
req.reply(commentsJson.filter((c) => c.id != 10)) // 3
|
||||
req.reply(notesJson.filter((c) => c.id != 10)) // 3
|
||||
}
|
||||
)
|
||||
|
||||
cy.intercept(
|
||||
'DELETE',
|
||||
'http://localhost:8000/api/documents/1/comments/?id=9',
|
||||
'http://localhost:8000/api/documents/1/notes/?id=9',
|
||||
(req) => {
|
||||
req.reply(commentsJson.filter((c) => c.id != 9 && c.id != 10)) // 2
|
||||
req.reply(notesJson.filter((c) => c.id != 9 && c.id != 10)) // 2
|
||||
}
|
||||
)
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
'http://localhost:8000/api/documents/1/comments/',
|
||||
'http://localhost:8000/api/documents/1/notes/',
|
||||
(req) => {
|
||||
req.reply(commentsJson) // 4
|
||||
req.reply(notesJson) // 4
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -75,33 +75,40 @@ describe('document-detail', () => {
|
||||
cy.get('pdf-viewer').should('be.visible')
|
||||
})
|
||||
|
||||
it('should show a list of comments', () => {
|
||||
cy.wait(1000)
|
||||
.get('a')
|
||||
.contains('Comments')
|
||||
.click({ force: true })
|
||||
.wait(1000)
|
||||
cy.get('app-document-comments').find('.card').its('length').should('eq', 3)
|
||||
it('should show a list of notes', () => {
|
||||
cy.wait(1000).get('a').contains('Notes').click({ force: true }).wait(1000)
|
||||
cy.get('app-document-notes').find('.card').its('length').should('eq', 3)
|
||||
})
|
||||
|
||||
it('should support comment deletion', () => {
|
||||
cy.wait(1000).get('a').contains('Comments').click().wait(1000)
|
||||
cy.get('app-document-comments')
|
||||
it('should support note deletion', () => {
|
||||
cy.wait(1000).get('a').contains('Notes').click().wait(1000)
|
||||
cy.get('app-document-notes')
|
||||
.find('.card')
|
||||
.first()
|
||||
.find('button')
|
||||
.click({ force: true })
|
||||
.wait(500)
|
||||
cy.get('app-document-comments').find('.card').its('length').should('eq', 2)
|
||||
cy.get('app-document-notes').find('.card').its('length').should('eq', 2)
|
||||
})
|
||||
|
||||
it('should support comment insertion', () => {
|
||||
cy.wait(1000).get('a').contains('Comments').click().wait(1000)
|
||||
cy.get('app-document-comments')
|
||||
it('should support note insertion', () => {
|
||||
cy.wait(1000).get('a').contains('Notes').click().wait(1000)
|
||||
cy.get('app-document-notes')
|
||||
.find('form textarea')
|
||||
.type('Testing new comment')
|
||||
.type('Testing new note')
|
||||
.wait(500)
|
||||
cy.get('app-document-comments').find('form button').click().wait(1500)
|
||||
cy.get('app-document-comments').find('.card').its('length').should('eq', 4)
|
||||
cy.get('app-document-notes').find('form button').click().wait(1500)
|
||||
cy.get('app-document-notes').find('.card').its('length').should('eq', 4)
|
||||
})
|
||||
|
||||
it('should support navigation to notes tab by url', () => {
|
||||
cy.visit('/documents/1/notes')
|
||||
cy.get('app-document-notes').should('exist')
|
||||
})
|
||||
|
||||
it('should dynamically update note counts', () => {
|
||||
cy.visit('/documents/1/notes')
|
||||
cy.get('app-document-notes').within(() => cy.contains('Delete').click())
|
||||
cy.get('ul.nav').find('li').contains('Notes').find('.badge').contains('2')
|
||||
})
|
||||
})
|
||||
|
@@ -48,6 +48,26 @@ describe('documents-list', () => {
|
||||
(d.tags as Array<number>).includes(tag_id)
|
||||
)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('correspondent__id__in')) {
|
||||
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__in=9,14
|
||||
const correspondent_ids = req.query['correspondent__id__in']
|
||||
.toString()
|
||||
.split(',')
|
||||
.map((c) => +c)
|
||||
response.results = (documentsJson.results as Array<any>).filter((d) =>
|
||||
correspondent_ids.includes(d.correspondent)
|
||||
)
|
||||
response.count = response.results.length
|
||||
} else if (req.query.hasOwnProperty('correspondent__id__none')) {
|
||||
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__none=9,14
|
||||
const correspondent_ids = req.query['correspondent__id__none']
|
||||
.toString()
|
||||
.split(',')
|
||||
.map((c) => +c)
|
||||
response.results = (documentsJson.results as Array<any>).filter(
|
||||
(d) => !correspondent_ids.includes(d.correspondent)
|
||||
)
|
||||
response.count = response.results.length
|
||||
}
|
||||
|
||||
req.reply(response)
|
||||
@@ -112,6 +132,27 @@ describe('documents-list', () => {
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should filter including multiple correspondents', () => {
|
||||
cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]')
|
||||
.click()
|
||||
.within(() => {
|
||||
cy.contains('button', 'ABC Test Correspondent').click()
|
||||
cy.contains('button', 'Corresp 11').click()
|
||||
})
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
it('should filter excluding multiple correspondents', () => {
|
||||
cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]')
|
||||
.click()
|
||||
.within(() => {
|
||||
cy.contains('button', 'ABC Test Correspondent').click()
|
||||
cy.contains('button', 'Corresp 11').click()
|
||||
cy.contains('label', 'Exclude').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(
|
||||
|
@@ -232,6 +232,11 @@ describe('documents query params', () => {
|
||||
|
||||
it('should show a list of documents filtered by document type', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&document_type__id=1')
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by multiple correspondents', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&document_type__id__in=1,2')
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
@@ -245,9 +250,14 @@ describe('documents query params', () => {
|
||||
cy.contains('2 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by multiple correspondents', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&correspondent__id__in=9,14')
|
||||
cy.contains('3 documents')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by no correspondent', () => {
|
||||
cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1')
|
||||
cy.contains('2 documents')
|
||||
cy.contains('One document')
|
||||
})
|
||||
|
||||
it('should show a list of documents filtered by storage path', () => {
|
||||
|
@@ -115,7 +115,7 @@ describe('settings', () => {
|
||||
cy.contains('a', 'Dashboard').click()
|
||||
cy.contains('You have unsaved changes')
|
||||
cy.contains('button', 'Cancel').click()
|
||||
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||
cy.contains('button', 'Save').click().wait(2000)
|
||||
cy.contains('a', 'Dashboard').click()
|
||||
cy.contains('You have unsaved changes').should('not.exist')
|
||||
})
|
||||
|
@@ -1 +1,257 @@
|
||||
{"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"}]}
|
||||
{
|
||||
"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": "correspondent-slug",
|
||||
"name": "Correspondent Slug",
|
||||
"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 +1,25 @@
|
||||
{"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}]}
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"slug": "test",
|
||||
"name": "Test Doc Type",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"document_count": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"slug": "test2",
|
||||
"name": "Test Doc Type 2",
|
||||
"match": "",
|
||||
"matching_algorithm": 1,
|
||||
"is_insensitive": true,
|
||||
"document_count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,46 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 10,
|
||||
"comment": "Testing new comment",
|
||||
"created": "2022-08-08T04:24:55.176008Z",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "user2",
|
||||
"firstname": "",
|
||||
"lastname": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"comment": "Testing one more time",
|
||||
"created": "2022-02-18T04:24:55.176008Z",
|
||||
"user": {
|
||||
"id": 2,
|
||||
"username": "user1",
|
||||
"firstname": "",
|
||||
"lastname": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"comment": "Another comment",
|
||||
"created": "2021-11-08T04:24:47.925042Z",
|
||||
"user": {
|
||||
"id": 2,
|
||||
"username": "user33",
|
||||
"firstname": "",
|
||||
"lastname": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"comment": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||
"created": "2021-02-08T02:37:49.724132Z",
|
||||
"user": {
|
||||
"id": 3,
|
||||
"username": "admin",
|
||||
"firstname": "",
|
||||
"lastname": ""
|
||||
}
|
||||
}
|
||||
]
|
26
src-ui/cypress/fixtures/documents/1/notes.json
Normal file
26
src-ui/cypress/fixtures/documents/1/notes.json
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": 10,
|
||||
"note": "Testing new note",
|
||||
"created": "2022-08-08T04:24:55.176008Z",
|
||||
"user": 3
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"note": "Testing one more time",
|
||||
"created": "2022-02-18T04:24:55.176008Z",
|
||||
"user": 15
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"note": "Another note",
|
||||
"created": "2021-11-08T04:24:47.925042Z",
|
||||
"user": 3
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"note": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||
"created": "2021-02-08T02:37:49.724132Z",
|
||||
"user": 3
|
||||
}
|
||||
]
|
@@ -14,11 +14,43 @@
|
||||
4
|
||||
],
|
||||
"created": "2022-03-22T07:24:18Z",
|
||||
"created_date": "2022-03-22",
|
||||
"modified": "2022-03-22T07:24:23.264859Z",
|
||||
"added": "2022-03-22T07:24:22.922631Z",
|
||||
"archive_serial_number": null,
|
||||
"original_file_name": "2022-03-22 no latin title.pdf",
|
||||
"archived_file_name": "2022-03-22 no latin title.pdf"
|
||||
"archived_file_name": "2022-03-22 no latin title.pdf",
|
||||
"owner": null,
|
||||
"permissions": {
|
||||
"view": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
},
|
||||
"change": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"id": 9,
|
||||
"note": "Testing one more time",
|
||||
"created": "2022-02-18T04:24:55.176008Z",
|
||||
"user": 15
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"note": "Another note",
|
||||
"created": "2021-11-08T04:24:47.925042Z",
|
||||
"user": 3
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"note": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||
"created": "2021-02-08T02:37:49.724132Z",
|
||||
"user": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -29,15 +61,28 @@
|
||||
"content": "Test document PDF",
|
||||
"tags": [],
|
||||
"created": "2022-03-23T07:24:18Z",
|
||||
"created_date": "2022-03-23",
|
||||
"modified": "2022-03-23T07:24:23.264859Z",
|
||||
"added": "2022-03-23T07:24:22.922631Z",
|
||||
"archive_serial_number": 12345,
|
||||
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
||||
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf"
|
||||
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf",
|
||||
"owner": null,
|
||||
"permissions": {
|
||||
"view": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
},
|
||||
"change": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
"notes": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"correspondent": null,
|
||||
"correspondent": 14,
|
||||
"document_type": 1,
|
||||
"storage_path": null,
|
||||
"title": "dolor",
|
||||
@@ -46,16 +91,29 @@
|
||||
2
|
||||
],
|
||||
"created": "2022-03-24T07:24:18Z",
|
||||
"created_date": "2022-03-24",
|
||||
"modified": "2022-03-24T07:24:23.264859Z",
|
||||
"added": "2022-03-24T07:24:22.922631Z",
|
||||
"archive_serial_number": null,
|
||||
"original_file_name": "2022-03-24 dolor.pdf",
|
||||
"archived_file_name": "2022-03-24 dolor.pdf"
|
||||
"archived_file_name": "2022-03-24 dolor.pdf",
|
||||
"owner": null,
|
||||
"permissions": {
|
||||
"view": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
},
|
||||
"change": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
"notes": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"correspondent": 9,
|
||||
"document_type": 1,
|
||||
"document_type": 2,
|
||||
"storage_path": null,
|
||||
"title": "sit amet",
|
||||
"content": "Test document PDF",
|
||||
@@ -63,11 +121,24 @@
|
||||
4, 5
|
||||
],
|
||||
"created": "2022-06-01T07:24:18Z",
|
||||
"created_date": "2022-06-01",
|
||||
"modified": "2022-06-01T07:24:23.264859Z",
|
||||
"added": "2022-06-01T07:24:22.922631Z",
|
||||
"archive_serial_number": 12347,
|
||||
"original_file_name": "2022-06-01 sit amet.pdf",
|
||||
"archived_file_name": "2022-06-01 sit amet.pdf"
|
||||
"archived_file_name": "2022-06-01 sit amet.pdf",
|
||||
"owner": null,
|
||||
"permissions": {
|
||||
"view": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
},
|
||||
"change": {
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
119
src-ui/cypress/fixtures/groups/groups.json
Normal file
119
src-ui/cypress/fixtures/groups/groups.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Another Group",
|
||||
"permissions": [
|
||||
"add_user",
|
||||
"change_user",
|
||||
"delete_user",
|
||||
"view_user",
|
||||
"add_note",
|
||||
"change_note",
|
||||
"delete_note",
|
||||
"view_note"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "First Group",
|
||||
"permissions": [
|
||||
"add_group",
|
||||
"change_group",
|
||||
"delete_group",
|
||||
"view_group",
|
||||
"add_permission",
|
||||
"change_permission",
|
||||
"delete_permission",
|
||||
"view_permission",
|
||||
"add_token",
|
||||
"change_token",
|
||||
"delete_token",
|
||||
"view_token",
|
||||
"add_tokenproxy",
|
||||
"change_tokenproxy",
|
||||
"delete_tokenproxy",
|
||||
"view_tokenproxy",
|
||||
"add_contenttype",
|
||||
"change_contenttype",
|
||||
"delete_contenttype",
|
||||
"view_contenttype",
|
||||
"add_chordcounter",
|
||||
"change_chordcounter",
|
||||
"delete_chordcounter",
|
||||
"view_chordcounter",
|
||||
"add_groupresult",
|
||||
"change_groupresult",
|
||||
"delete_groupresult",
|
||||
"view_groupresult",
|
||||
"add_taskresult",
|
||||
"change_taskresult",
|
||||
"delete_taskresult",
|
||||
"view_taskresult",
|
||||
"add_failure",
|
||||
"change_failure",
|
||||
"delete_failure",
|
||||
"view_failure",
|
||||
"add_ormq",
|
||||
"change_ormq",
|
||||
"delete_ormq",
|
||||
"view_ormq",
|
||||
"add_schedule",
|
||||
"change_schedule",
|
||||
"delete_schedule",
|
||||
"view_schedule",
|
||||
"add_success",
|
||||
"change_success",
|
||||
"delete_success",
|
||||
"view_success",
|
||||
"add_task",
|
||||
"change_task",
|
||||
"delete_task",
|
||||
"view_task",
|
||||
"add_note",
|
||||
"change_note",
|
||||
"delete_note",
|
||||
"view_note",
|
||||
"add_correspondent",
|
||||
"change_correspondent",
|
||||
"delete_correspondent",
|
||||
"view_correspondent",
|
||||
"add_document",
|
||||
"change_document",
|
||||
"delete_document",
|
||||
"view_document",
|
||||
"add_documenttype",
|
||||
"change_documenttype",
|
||||
"delete_documenttype",
|
||||
"view_documenttype",
|
||||
"add_frontendsettings",
|
||||
"change_frontendsettings",
|
||||
"delete_frontendsettings",
|
||||
"view_frontendsettings",
|
||||
"add_log",
|
||||
"change_log",
|
||||
"delete_log",
|
||||
"view_log",
|
||||
"add_savedview",
|
||||
"change_savedview",
|
||||
"delete_savedview",
|
||||
"view_savedview",
|
||||
"add_savedviewfilterrule",
|
||||
"change_savedviewfilterrule",
|
||||
"delete_savedviewfilterrule",
|
||||
"view_savedviewfilterrule",
|
||||
"add_taskattributes",
|
||||
"change_taskattributes",
|
||||
"delete_taskattributes",
|
||||
"view_taskattributes",
|
||||
"add_session",
|
||||
"change_session",
|
||||
"delete_session",
|
||||
"view_session"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
"account": 2,
|
||||
"folder": "INBOX",
|
||||
"filter_from": null,
|
||||
"filter_to": null,
|
||||
"filter_subject": "[paperless]",
|
||||
"filter_body": null,
|
||||
"filter_attachment_filename": null,
|
||||
|
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"display_name": "Admin",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"is_superuser": true,
|
||||
"groups": []
|
||||
},
|
||||
"settings": {
|
||||
"language": "",
|
||||
"bulk_edit": {
|
||||
@@ -30,5 +33,131 @@
|
||||
"consumer_failed": true,
|
||||
"consumer_suppress_on_dashboard": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"add_logentry",
|
||||
"change_logentry",
|
||||
"delete_logentry",
|
||||
"view_logentry",
|
||||
"add_group",
|
||||
"change_group",
|
||||
"delete_group",
|
||||
"view_group",
|
||||
"add_permission",
|
||||
"change_permission",
|
||||
"delete_permission",
|
||||
"view_permission",
|
||||
"add_user",
|
||||
"change_user",
|
||||
"delete_user",
|
||||
"view_user",
|
||||
"add_token",
|
||||
"change_token",
|
||||
"delete_token",
|
||||
"view_token",
|
||||
"add_tokenproxy",
|
||||
"change_tokenproxy",
|
||||
"delete_tokenproxy",
|
||||
"view_tokenproxy",
|
||||
"add_contenttype",
|
||||
"change_contenttype",
|
||||
"delete_contenttype",
|
||||
"view_contenttype",
|
||||
"add_chordcounter",
|
||||
"change_chordcounter",
|
||||
"delete_chordcounter",
|
||||
"view_chordcounter",
|
||||
"add_groupresult",
|
||||
"change_groupresult",
|
||||
"delete_groupresult",
|
||||
"view_groupresult",
|
||||
"add_taskresult",
|
||||
"change_taskresult",
|
||||
"delete_taskresult",
|
||||
"view_taskresult",
|
||||
"add_failure",
|
||||
"change_failure",
|
||||
"delete_failure",
|
||||
"view_failure",
|
||||
"add_ormq",
|
||||
"change_ormq",
|
||||
"delete_ormq",
|
||||
"view_ormq",
|
||||
"add_schedule",
|
||||
"change_schedule",
|
||||
"delete_schedule",
|
||||
"view_schedule",
|
||||
"add_success",
|
||||
"change_success",
|
||||
"delete_success",
|
||||
"view_success",
|
||||
"add_task",
|
||||
"change_task",
|
||||
"delete_task",
|
||||
"view_task",
|
||||
"add_note",
|
||||
"change_note",
|
||||
"delete_note",
|
||||
"view_note",
|
||||
"add_correspondent",
|
||||
"change_correspondent",
|
||||
"delete_correspondent",
|
||||
"view_correspondent",
|
||||
"add_document",
|
||||
"change_document",
|
||||
"delete_document",
|
||||
"view_document",
|
||||
"add_documenttype",
|
||||
"change_documenttype",
|
||||
"delete_documenttype",
|
||||
"view_documenttype",
|
||||
"add_frontendsettings",
|
||||
"change_frontendsettings",
|
||||
"delete_frontendsettings",
|
||||
"view_frontendsettings",
|
||||
"add_log",
|
||||
"change_log",
|
||||
"delete_log",
|
||||
"view_log",
|
||||
"add_paperlesstask",
|
||||
"change_paperlesstask",
|
||||
"delete_paperlesstask",
|
||||
"view_paperlesstask",
|
||||
"add_savedview",
|
||||
"change_savedview",
|
||||
"delete_savedview",
|
||||
"view_savedview",
|
||||
"add_savedviewfilterrule",
|
||||
"change_savedviewfilterrule",
|
||||
"delete_savedviewfilterrule",
|
||||
"view_savedviewfilterrule",
|
||||
"add_storagepath",
|
||||
"change_storagepath",
|
||||
"delete_storagepath",
|
||||
"view_storagepath",
|
||||
"add_tag",
|
||||
"change_tag",
|
||||
"delete_tag",
|
||||
"view_tag",
|
||||
"add_taskattributes",
|
||||
"change_taskattributes",
|
||||
"delete_taskattributes",
|
||||
"view_taskattributes",
|
||||
"add_uisettings",
|
||||
"change_uisettings",
|
||||
"delete_uisettings",
|
||||
"view_uisettings",
|
||||
"add_mailaccount",
|
||||
"change_mailaccount",
|
||||
"delete_mailaccount",
|
||||
"view_mailaccount",
|
||||
"add_mailrule",
|
||||
"change_mailrule",
|
||||
"delete_mailrule",
|
||||
"view_mailrule",
|
||||
"add_session",
|
||||
"change_session",
|
||||
"delete_session",
|
||||
"view_session"
|
||||
]
|
||||
}
|
||||
|
88
src-ui/cypress/fixtures/ui_settings/settings_restricted.json
Normal file
88
src-ui/cypress/fixtures/ui_settings/settings_restricted.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"is_superuser": false,
|
||||
"groups": []
|
||||
},
|
||||
"settings": {
|
||||
"language": "",
|
||||
"bulk_edit": {
|
||||
"confirmation_dialogs": true,
|
||||
"apply_on_close": false
|
||||
},
|
||||
"documentListSize": 50,
|
||||
"dark_mode": {
|
||||
"use_system": true,
|
||||
"enabled": "false",
|
||||
"thumb_inverted": "true"
|
||||
},
|
||||
"theme": {
|
||||
"color": "#b198e5"
|
||||
},
|
||||
"document_details": {
|
||||
"native_pdf_viewer": false
|
||||
},
|
||||
"date_display": {
|
||||
"date_locale": "",
|
||||
"date_format": "mediumDate"
|
||||
},
|
||||
"notifications": {
|
||||
"consumer_new_documents": true,
|
||||
"consumer_success": true,
|
||||
"consumer_failed": true,
|
||||
"consumer_suppress_on_dashboard": true
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"add_token",
|
||||
"change_token",
|
||||
"delete_token",
|
||||
"view_token",
|
||||
"add_tokenproxy",
|
||||
"change_tokenproxy",
|
||||
"delete_tokenproxy",
|
||||
"view_tokenproxy",
|
||||
"add_contenttype",
|
||||
"change_contenttype",
|
||||
"delete_contenttype",
|
||||
"view_contenttype",
|
||||
"add_chordcounter",
|
||||
"change_chordcounter",
|
||||
"delete_chordcounter",
|
||||
"view_chordcounter",
|
||||
"add_groupresult",
|
||||
"change_groupresult",
|
||||
"delete_groupresult",
|
||||
"view_groupresult",
|
||||
"add_failure",
|
||||
"change_failure",
|
||||
"delete_failure",
|
||||
"view_failure",
|
||||
"add_ormq",
|
||||
"change_ormq",
|
||||
"delete_ormq",
|
||||
"view_ormq",
|
||||
"add_schedule",
|
||||
"change_schedule",
|
||||
"delete_schedule",
|
||||
"view_schedule",
|
||||
"add_success",
|
||||
"change_success",
|
||||
"delete_success",
|
||||
"view_success",
|
||||
"add_task",
|
||||
"change_task",
|
||||
"delete_task",
|
||||
"view_task",
|
||||
"add_note",
|
||||
"add_frontendsettings",
|
||||
"change_frontendsettings",
|
||||
"delete_frontendsettings",
|
||||
"view_frontendsettings",
|
||||
"add_session",
|
||||
"change_session",
|
||||
"delete_session",
|
||||
"view_session"
|
||||
]
|
||||
}
|
459
src-ui/cypress/fixtures/users/users.json
Normal file
459
src-ui/cypress/fixtures/users/users.json
Normal file
@@ -0,0 +1,459 @@
|
||||
{
|
||||
"count": 4,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 3,
|
||||
"username": "admin",
|
||||
"password": "**********",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"date_joined": "2022-02-14T23:11:09.103293Z",
|
||||
"is_staff": true,
|
||||
"is_active": true,
|
||||
"is_superuser": true,
|
||||
"groups": [],
|
||||
"user_permissions": [],
|
||||
"inherited_permissions": [
|
||||
"auth.delete_permission",
|
||||
"paperless_mail.change_mailrule",
|
||||
"django_celery_results.add_taskresult",
|
||||
"documents.view_taskattributes",
|
||||
"documents.view_paperlesstask",
|
||||
"django_q.add_success",
|
||||
"documents.view_uisettings",
|
||||
"auth.change_user",
|
||||
"admin.delete_logentry",
|
||||
"django_celery_results.change_taskresult",
|
||||
"django_q.change_schedule",
|
||||
"django_celery_results.delete_taskresult",
|
||||
"paperless_mail.add_mailaccount",
|
||||
"auth.change_group",
|
||||
"documents.add_note",
|
||||
"paperless_mail.delete_mailaccount",
|
||||
"authtoken.delete_tokenproxy",
|
||||
"guardian.delete_groupobjectpermission",
|
||||
"contenttypes.delete_contenttype",
|
||||
"documents.change_correspondent",
|
||||
"authtoken.delete_token",
|
||||
"documents.delete_documenttype",
|
||||
"django_q.change_ormq",
|
||||
"documents.change_savedviewfilterrule",
|
||||
"auth.delete_group",
|
||||
"documents.add_documenttype",
|
||||
"django_q.change_success",
|
||||
"documents.delete_tag",
|
||||
"documents.change_note",
|
||||
"django_q.delete_task",
|
||||
"documents.add_savedviewfilterrule",
|
||||
"django_q.view_task",
|
||||
"paperless_mail.add_mailrule",
|
||||
"paperless_mail.view_mailaccount",
|
||||
"documents.add_frontendsettings",
|
||||
"sessions.change_session",
|
||||
"documents.view_savedview",
|
||||
"authtoken.add_tokenproxy",
|
||||
"documents.change_tag",
|
||||
"documents.view_document",
|
||||
"documents.add_savedview",
|
||||
"auth.delete_user",
|
||||
"documents.view_log",
|
||||
"documents.view_note",
|
||||
"guardian.change_groupobjectpermission",
|
||||
"sessions.delete_session",
|
||||
"django_q.change_failure",
|
||||
"guardian.change_userobjectpermission",
|
||||
"documents.change_storagepath",
|
||||
"documents.delete_document",
|
||||
"documents.delete_taskattributes",
|
||||
"django_celery_results.change_groupresult",
|
||||
"django_q.add_ormq",
|
||||
"guardian.view_groupobjectpermission",
|
||||
"admin.change_logentry",
|
||||
"django_q.delete_schedule",
|
||||
"documents.delete_paperlesstask",
|
||||
"django_q.view_ormq",
|
||||
"documents.change_paperlesstask",
|
||||
"guardian.delete_userobjectpermission",
|
||||
"auth.view_permission",
|
||||
"auth.view_user",
|
||||
"django_q.add_schedule",
|
||||
"authtoken.change_token",
|
||||
"guardian.add_groupobjectpermission",
|
||||
"documents.view_documenttype",
|
||||
"documents.change_log",
|
||||
"paperless_mail.delete_mailrule",
|
||||
"auth.view_group",
|
||||
"authtoken.view_token",
|
||||
"admin.view_logentry",
|
||||
"django_celery_results.view_chordcounter",
|
||||
"django_celery_results.view_groupresult",
|
||||
"documents.view_storagepath",
|
||||
"documents.add_storagepath",
|
||||
"django_celery_results.add_groupresult",
|
||||
"documents.view_tag",
|
||||
"guardian.view_userobjectpermission",
|
||||
"documents.delete_correspondent",
|
||||
"documents.add_tag",
|
||||
"documents.delete_savedviewfilterrule",
|
||||
"documents.add_correspondent",
|
||||
"authtoken.view_tokenproxy",
|
||||
"documents.delete_frontendsettings",
|
||||
"django_celery_results.delete_chordcounter",
|
||||
"django_q.change_task",
|
||||
"documents.add_taskattributes",
|
||||
"documents.delete_storagepath",
|
||||
"sessions.add_session",
|
||||
"documents.add_uisettings",
|
||||
"documents.change_taskattributes",
|
||||
"documents.delete_uisettings",
|
||||
"django_q.delete_ormq",
|
||||
"auth.change_permission",
|
||||
"documents.view_savedviewfilterrule",
|
||||
"documents.change_frontendsettings",
|
||||
"documents.change_documenttype",
|
||||
"documents.view_correspondent",
|
||||
"auth.add_user",
|
||||
"paperless_mail.change_mailaccount",
|
||||
"documents.add_paperlesstask",
|
||||
"django_q.view_success",
|
||||
"django_celery_results.delete_groupresult",
|
||||
"documents.delete_savedview",
|
||||
"authtoken.change_tokenproxy",
|
||||
"documents.view_frontendsettings",
|
||||
"authtoken.add_token",
|
||||
"django_celery_results.add_chordcounter",
|
||||
"contenttypes.change_contenttype",
|
||||
"admin.add_logentry",
|
||||
"django_q.delete_failure",
|
||||
"documents.change_uisettings",
|
||||
"django_q.view_failure",
|
||||
"documents.add_log",
|
||||
"documents.change_savedview",
|
||||
"paperless_mail.view_mailrule",
|
||||
"django_q.view_schedule",
|
||||
"documents.change_document",
|
||||
"django_celery_results.change_chordcounter",
|
||||
"documents.add_document",
|
||||
"django_celery_results.view_taskresult",
|
||||
"contenttypes.add_contenttype",
|
||||
"django_q.delete_success",
|
||||
"documents.delete_note",
|
||||
"django_q.add_failure",
|
||||
"guardian.add_userobjectpermission",
|
||||
"sessions.view_session",
|
||||
"contenttypes.view_contenttype",
|
||||
"auth.add_permission",
|
||||
"documents.delete_log",
|
||||
"django_q.add_task",
|
||||
"auth.add_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"username": "test",
|
||||
"password": "**********",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"date_joined": "2022-11-23T08:30:54Z",
|
||||
"is_staff": true,
|
||||
"is_active": true,
|
||||
"is_superuser": false,
|
||||
"groups": [
|
||||
1
|
||||
],
|
||||
"user_permissions": [
|
||||
"add_group",
|
||||
"change_group",
|
||||
"delete_group",
|
||||
"view_group",
|
||||
"add_permission",
|
||||
"change_permission",
|
||||
"delete_permission",
|
||||
"view_permission",
|
||||
"add_token",
|
||||
"change_token",
|
||||
"delete_token",
|
||||
"view_token",
|
||||
"add_tokenproxy",
|
||||
"change_tokenproxy",
|
||||
"delete_tokenproxy",
|
||||
"view_tokenproxy",
|
||||
"add_contenttype",
|
||||
"change_contenttype",
|
||||
"delete_contenttype",
|
||||
"view_contenttype",
|
||||
"add_chordcounter",
|
||||
"change_chordcounter",
|
||||
"delete_chordcounter",
|
||||
"view_chordcounter",
|
||||
"add_groupresult",
|
||||
"change_groupresult",
|
||||
"delete_groupresult",
|
||||
"view_groupresult",
|
||||
"add_taskresult",
|
||||
"change_taskresult",
|
||||
"delete_taskresult",
|
||||
"view_taskresult",
|
||||
"add_failure",
|
||||
"change_failure",
|
||||
"delete_failure",
|
||||
"view_failure",
|
||||
"add_ormq",
|
||||
"change_ormq",
|
||||
"delete_ormq",
|
||||
"view_ormq",
|
||||
"add_schedule",
|
||||
"change_schedule",
|
||||
"delete_schedule",
|
||||
"view_schedule",
|
||||
"add_success",
|
||||
"change_success",
|
||||
"delete_success",
|
||||
"view_success",
|
||||
"add_task",
|
||||
"change_task",
|
||||
"delete_task",
|
||||
"view_task",
|
||||
"add_note",
|
||||
"change_note",
|
||||
"delete_note",
|
||||
"view_note",
|
||||
"add_frontendsettings",
|
||||
"change_frontendsettings",
|
||||
"delete_frontendsettings",
|
||||
"view_frontendsettings",
|
||||
"add_log",
|
||||
"change_log",
|
||||
"delete_log",
|
||||
"view_log",
|
||||
"add_savedviewfilterrule",
|
||||
"change_savedviewfilterrule",
|
||||
"delete_savedviewfilterrule",
|
||||
"view_savedviewfilterrule",
|
||||
"add_taskattributes",
|
||||
"change_taskattributes",
|
||||
"delete_taskattributes",
|
||||
"view_taskattributes",
|
||||
"add_session",
|
||||
"change_session",
|
||||
"delete_session",
|
||||
"view_session"
|
||||
],
|
||||
"inherited_permissions": [
|
||||
"auth.delete_permission",
|
||||
"django_celery_results.add_taskresult",
|
||||
"documents.view_taskattributes",
|
||||
"django_q.add_ormq",
|
||||
"django_q.add_success",
|
||||
"django_q.delete_schedule",
|
||||
"django_q.view_ormq",
|
||||
"auth.view_permission",
|
||||
"django_q.add_schedule",
|
||||
"django_celery_results.change_taskresult",
|
||||
"django_q.change_schedule",
|
||||
"django_celery_results.delete_taskresult",
|
||||
"authtoken.change_token",
|
||||
"auth.change_group",
|
||||
"documents.add_note",
|
||||
"authtoken.delete_tokenproxy",
|
||||
"documents.view_documenttype",
|
||||
"contenttypes.delete_contenttype",
|
||||
"documents.change_correspondent",
|
||||
"authtoken.delete_token",
|
||||
"documents.change_log",
|
||||
"auth.view_group",
|
||||
"authtoken.view_token",
|
||||
"django_celery_results.view_chordcounter",
|
||||
"django_celery_results.view_groupresult",
|
||||
"documents.delete_documenttype",
|
||||
"django_q.change_ormq",
|
||||
"documents.change_savedviewfilterrule",
|
||||
"django_celery_results.add_groupresult",
|
||||
"auth.delete_group",
|
||||
"documents.add_documenttype",
|
||||
"django_q.change_success",
|
||||
"auth.add_permission",
|
||||
"documents.delete_correspondent",
|
||||
"documents.delete_savedviewfilterrule",
|
||||
"documents.add_correspondent",
|
||||
"authtoken.view_tokenproxy",
|
||||
"documents.delete_frontendsettings",
|
||||
"django_celery_results.delete_chordcounter",
|
||||
"documents.add_taskattributes",
|
||||
"django_q.change_task",
|
||||
"sessions.add_session",
|
||||
"documents.change_taskattributes",
|
||||
"documents.change_note",
|
||||
"django_q.delete_task",
|
||||
"django_q.delete_ormq",
|
||||
"auth.change_permission",
|
||||
"documents.add_savedviewfilterrule",
|
||||
"django_q.view_task",
|
||||
"documents.view_savedviewfilterrule",
|
||||
"documents.change_frontendsettings",
|
||||
"documents.change_documenttype",
|
||||
"documents.view_correspondent",
|
||||
"django_q.view_success",
|
||||
"documents.add_frontendsettings",
|
||||
"django_celery_results.delete_groupresult",
|
||||
"documents.delete_savedview",
|
||||
"authtoken.change_tokenproxy",
|
||||
"documents.view_frontendsettings",
|
||||
"authtoken.add_token",
|
||||
"sessions.change_session",
|
||||
"django_celery_results.add_chordcounter",
|
||||
"documents.view_savedview",
|
||||
"contenttypes.change_contenttype",
|
||||
"django_q.delete_failure",
|
||||
"authtoken.add_tokenproxy",
|
||||
"documents.view_document",
|
||||
"documents.add_savedview",
|
||||
"django_q.view_failure",
|
||||
"documents.view_note",
|
||||
"documents.view_log",
|
||||
"documents.add_log",
|
||||
"documents.change_savedview",
|
||||
"django_q.view_schedule",
|
||||
"documents.change_document",
|
||||
"django_celery_results.change_chordcounter",
|
||||
"documents.add_document",
|
||||
"sessions.delete_session",
|
||||
"django_q.change_failure",
|
||||
"django_celery_results.view_taskresult",
|
||||
"contenttypes.add_contenttype",
|
||||
"django_q.delete_success",
|
||||
"documents.delete_note",
|
||||
"django_q.add_failure",
|
||||
"sessions.view_session",
|
||||
"contenttypes.view_contenttype",
|
||||
"documents.delete_taskattributes",
|
||||
"documents.delete_document",
|
||||
"documents.delete_log",
|
||||
"django_q.add_task",
|
||||
"django_celery_results.change_groupresult",
|
||||
"auth.add_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"username": "testuser",
|
||||
"password": "**********",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"date_joined": "2022-11-16T04:14:20.484914Z",
|
||||
"is_staff": false,
|
||||
"is_active": true,
|
||||
"is_superuser": false,
|
||||
"groups": [
|
||||
1,
|
||||
6
|
||||
],
|
||||
"user_permissions": [
|
||||
"add_logentry",
|
||||
"change_logentry",
|
||||
"delete_logentry",
|
||||
"view_logentry"
|
||||
],
|
||||
"inherited_permissions": [
|
||||
"auth.delete_permission",
|
||||
"django_celery_results.add_taskresult",
|
||||
"documents.view_taskattributes",
|
||||
"django_q.add_ormq",
|
||||
"django_q.add_success",
|
||||
"django_q.delete_schedule",
|
||||
"django_q.view_ormq",
|
||||
"auth.change_user",
|
||||
"auth.view_permission",
|
||||
"auth.view_user",
|
||||
"django_q.add_schedule",
|
||||
"django_celery_results.change_taskresult",
|
||||
"django_q.change_schedule",
|
||||
"django_celery_results.delete_taskresult",
|
||||
"authtoken.change_token",
|
||||
"auth.change_group",
|
||||
"documents.add_note",
|
||||
"authtoken.delete_tokenproxy",
|
||||
"documents.view_documenttype",
|
||||
"contenttypes.delete_contenttype",
|
||||
"documents.change_correspondent",
|
||||
"authtoken.delete_token",
|
||||
"documents.change_log",
|
||||
"auth.view_group",
|
||||
"authtoken.view_token",
|
||||
"django_celery_results.view_chordcounter",
|
||||
"django_celery_results.view_groupresult",
|
||||
"documents.delete_documenttype",
|
||||
"django_q.change_ormq",
|
||||
"documents.change_savedviewfilterrule",
|
||||
"django_celery_results.add_groupresult",
|
||||
"auth.delete_group",
|
||||
"documents.add_documenttype",
|
||||
"django_q.change_success",
|
||||
"auth.add_permission",
|
||||
"documents.delete_correspondent",
|
||||
"documents.delete_savedviewfilterrule",
|
||||
"documents.add_correspondent",
|
||||
"authtoken.view_tokenproxy",
|
||||
"documents.delete_frontendsettings",
|
||||
"django_celery_results.delete_chordcounter",
|
||||
"documents.add_taskattributes",
|
||||
"django_q.change_task",
|
||||
"sessions.add_session",
|
||||
"documents.change_taskattributes",
|
||||
"documents.change_note",
|
||||
"django_q.delete_task",
|
||||
"django_q.delete_ormq",
|
||||
"auth.change_permission",
|
||||
"documents.add_savedviewfilterrule",
|
||||
"django_q.view_task",
|
||||
"documents.view_savedviewfilterrule",
|
||||
"documents.change_frontendsettings",
|
||||
"documents.change_documenttype",
|
||||
"documents.view_correspondent",
|
||||
"auth.add_user",
|
||||
"django_q.view_success",
|
||||
"documents.add_frontendsettings",
|
||||
"django_celery_results.delete_groupresult",
|
||||
"documents.delete_savedview",
|
||||
"authtoken.change_tokenproxy",
|
||||
"documents.view_frontendsettings",
|
||||
"authtoken.add_token",
|
||||
"sessions.change_session",
|
||||
"django_celery_results.add_chordcounter",
|
||||
"documents.view_savedview",
|
||||
"contenttypes.change_contenttype",
|
||||
"django_q.delete_failure",
|
||||
"authtoken.add_tokenproxy",
|
||||
"documents.view_document",
|
||||
"documents.add_savedview",
|
||||
"django_q.view_failure",
|
||||
"documents.view_note",
|
||||
"documents.view_log",
|
||||
"auth.delete_user",
|
||||
"documents.add_log",
|
||||
"documents.change_savedview",
|
||||
"django_q.view_schedule",
|
||||
"documents.change_document",
|
||||
"django_celery_results.change_chordcounter",
|
||||
"documents.add_document",
|
||||
"sessions.delete_session",
|
||||
"django_q.change_failure",
|
||||
"django_celery_results.view_taskresult",
|
||||
"contenttypes.add_contenttype",
|
||||
"django_q.delete_success",
|
||||
"documents.delete_note",
|
||||
"django_q.add_failure",
|
||||
"sessions.view_session",
|
||||
"contenttypes.view_contenttype",
|
||||
"documents.delete_taskattributes",
|
||||
"documents.delete_document",
|
||||
"documents.delete_log",
|
||||
"django_q.add_task",
|
||||
"django_celery_results.change_groupresult",
|
||||
"auth.add_group"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -5,6 +5,14 @@ beforeEach(() => {
|
||||
fixture: 'ui_settings/settings.json',
|
||||
}).as('ui-settings')
|
||||
|
||||
cy.intercept('http://localhost:8000/api/users/*', {
|
||||
fixture: 'users/users.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/groups/*', {
|
||||
fixture: 'groups/groups.json',
|
||||
})
|
||||
|
||||
cy.intercept('http://localhost:8000/api/remote_version/', {
|
||||
fixture: 'remote_version/remote_version.json',
|
||||
})
|
||||
|
2366
src-ui/messages.xlf
2366
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
15221
src-ui/package-lock.json
generated
15221
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,20 +13,21 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "~15.1.0",
|
||||
"@angular/compiler": "~15.1.0",
|
||||
"@angular/core": "~15.1.0",
|
||||
"@angular/forms": "~15.1.0",
|
||||
"@angular/localize": "~15.1.0",
|
||||
"@angular/platform-browser": "~15.1.0",
|
||||
"@angular/platform-browser-dynamic": "~15.1.0",
|
||||
"@angular/router": "~15.1.0",
|
||||
"@angular/common": "~15.2.5",
|
||||
"@angular/compiler": "~15.2.5",
|
||||
"@angular/core": "~15.2.5",
|
||||
"@angular/forms": "~15.2.5",
|
||||
"@angular/localize": "~15.2.5",
|
||||
"@angular/platform-browser": "~15.2.5",
|
||||
"@angular/platform-browser-dynamic": "~15.2.5",
|
||||
"@angular/router": "~15.2.5",
|
||||
"@ng-bootstrap/ng-bootstrap": "^14.0.1",
|
||||
"@ng-select/ng-select": "^10.0.1",
|
||||
"@ng-select/ng-select": "^10.0.4",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "^5.2.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^9.1.2",
|
||||
"ngx-color": "^8.0.3",
|
||||
"ngx-cookie-service": "^15.0.0",
|
||||
@@ -35,22 +36,22 @@
|
||||
"rxjs": "^7.8.0",
|
||||
"tslib": "^2.4.1",
|
||||
"uuid": "^9.0.0",
|
||||
"zone.js": "~0.11.8"
|
||||
"zone.js": "~0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "15.0.0",
|
||||
"@angular-devkit/build-angular": "~15.1.0",
|
||||
"@angular-eslint/builder": "15.1.0",
|
||||
"@angular-eslint/eslint-plugin": "15.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "15.1.0",
|
||||
"@angular-eslint/schematics": "15.1.0",
|
||||
"@angular-eslint/template-parser": "15.1.0",
|
||||
"@angular/cli": "~15.1.0",
|
||||
"@angular/compiler-cli": "~15.1.0",
|
||||
"@angular-devkit/build-angular": "~15.2.4",
|
||||
"@angular-eslint/builder": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||
"@angular-eslint/schematics": "15.2.1",
|
||||
"@angular-eslint/template-parser": "15.2.1",
|
||||
"@angular/cli": "~15.2.4",
|
||||
"@angular/compiler-cli": "~15.2.5",
|
||||
"@types/jest": "28.1.6",
|
||||
"@types/node": "^18.7.23",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||
"@typescript-eslint/parser": "^5.54.0",
|
||||
"concurrently": "7.4.0",
|
||||
"eslint": "^8.31.0",
|
||||
"jest": "28.1.3",
|
||||
|
@@ -14,8 +14,13 @@ import { DocumentAsnComponent } from './components/document-asn/document-asn.com
|
||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
import { PermissionsGuard } from './guards/permissions.guard'
|
||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
} from './services/permissions.service'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@@ -29,28 +34,153 @@ const routes: Routes = [
|
||||
path: 'documents',
|
||||
component: DocumentListComponent,
|
||||
canDeactivate: [DirtySavedViewGuard],
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'view/:id',
|
||||
component: DocumentListComponent,
|
||||
canDeactivate: [DirtySavedViewGuard],
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.SavedView,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'documents/:id',
|
||||
component: DocumentDetailComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'documents/:id/:section',
|
||||
component: DocumentDetailComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'asn/:id',
|
||||
component: DocumentAsnComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tags',
|
||||
component: TagListComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Tag,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'documenttypes',
|
||||
component: DocumentTypeListComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.DocumentType,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'correspondents',
|
||||
component: CorrespondentListComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Correspondent,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'storagepaths',
|
||||
component: StoragePathListComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.StoragePath,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
component: LogsComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Admin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ path: 'documents/:id', component: DocumentDetailComponent },
|
||||
{ path: 'asn/:id', component: DocumentAsnComponent },
|
||||
{ path: 'tags', component: TagListComponent },
|
||||
{ path: 'documenttypes', component: DocumentTypeListComponent },
|
||||
{ path: 'correspondents', component: CorrespondentListComponent },
|
||||
{ path: 'storagepaths', component: StoragePathListComponent },
|
||||
{ path: 'logs', component: LogsComponent },
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
canDeactivate: [DirtyFormGuard],
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.UISettings,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'settings/:section',
|
||||
component: SettingsComponent,
|
||||
canDeactivate: [DirtyFormGuard],
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.UISettings,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'settings/:section',
|
||||
component: SettingsComponent,
|
||||
canDeactivate: [DirtyFormGuard],
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
component: TasksComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.PaperlessTask,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ path: 'tasks', component: TasksComponent },
|
||||
],
|
||||
|
@@ -9,6 +9,11 @@ import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||
import { TasksService } from './services/tasks.service'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from './services/permissions.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -32,7 +37,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
private tasksService: TasksService,
|
||||
public tourService: TourService,
|
||||
private renderer: Renderer2
|
||||
private renderer: Renderer2,
|
||||
private permissionsService: PermissionsService
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||
@@ -74,15 +80,28 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
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])
|
||||
},
|
||||
})
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Document
|
||||
)
|
||||
) {
|
||||
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])
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.toastService.show({
|
||||
title: $localize`Document added`,
|
||||
delay: 10000,
|
||||
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -225,7 +244,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public get dragDropEnabled(): boolean {
|
||||
return !this.router.url.includes('dashboard')
|
||||
return (
|
||||
!this.router.url.includes('dashboard') &&
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.Add,
|
||||
PermissionType.Document
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public fileOver() {
|
||||
|
@@ -42,6 +42,7 @@ import { CheckComponent } from './components/common/input/check/check.component'
|
||||
import { PasswordComponent } from './components/common/input/password/password.component'
|
||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
||||
import { TagsComponent } from './components/common/input/tags/tags.component'
|
||||
import { IfPermissionsDirective } from './directives/if-permissions.directive'
|
||||
import { SortableDirective } from './directives/sortable.directive'
|
||||
import { CookieService } from 'ngx-cookie-service'
|
||||
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
||||
@@ -69,7 +70,8 @@ import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
import { ColorComponent } from './components/common/input/color/color.component'
|
||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
||||
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
|
||||
import { PermissionsGuard } from './guards/permissions.guard'
|
||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
@@ -77,8 +79,15 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
|
||||
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
|
||||
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
|
||||
import { IfOwnerDirective } from './directives/if-owner.directive'
|
||||
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
|
||||
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
import localeBe from '@angular/common/locales/be'
|
||||
@@ -100,6 +109,8 @@ 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'
|
||||
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
|
||||
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
registerLocaleData(localeAr)
|
||||
registerLocaleData(localeBe)
|
||||
@@ -165,6 +176,7 @@ function initializeApp(settings: SettingsService) {
|
||||
PasswordComponent,
|
||||
SaveViewConfigDialogComponent,
|
||||
TagsComponent,
|
||||
IfPermissionsDirective,
|
||||
SortableDirective,
|
||||
SavedViewWidgetComponent,
|
||||
StatisticsWidgetComponent,
|
||||
@@ -184,10 +196,19 @@ function initializeApp(settings: SettingsService) {
|
||||
DateComponent,
|
||||
ColorComponent,
|
||||
DocumentAsnComponent,
|
||||
DocumentCommentsComponent,
|
||||
DocumentNotesComponent,
|
||||
TasksComponent,
|
||||
UserEditDialogComponent,
|
||||
GroupEditDialogComponent,
|
||||
PermissionsSelectComponent,
|
||||
MailAccountEditDialogComponent,
|
||||
MailRuleEditDialogComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
IfOwnerDirective,
|
||||
IfObjectPermissionsDirective,
|
||||
PermissionsDialogComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -225,6 +246,7 @@ function initializeApp(settings: SettingsService) {
|
||||
DocumentTitlePipe,
|
||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||
PermissionsGuard,
|
||||
DirtyDocGuard,
|
||||
DirtySavedViewGuard,
|
||||
],
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</svg>
|
||||
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
||||
</a>
|
||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
|
||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||
<svg width="1em" height="1em" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||
@@ -39,7 +39,7 @@
|
||||
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
||||
<div class="dropdown-divider"></div>
|
||||
</div>
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||
<svg class="sidebaricon me-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
</svg><ng-container i18n>Settings</ng-container>
|
||||
@@ -72,7 +72,7 @@
|
||||
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||
@@ -80,79 +80,82 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg><span> {{view.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg><span> {{view.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||
<span i18n>Open documents</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg><span> {{d.title | documentTitle}}</span>
|
||||
<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">
|
||||
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||
<span i18n>Open documents</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg><span> {{d.title | documentTitle}}</span>
|
||||
<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">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||
<span i18n>Manage</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.tags">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||
</svg><span> <ng-container i18n>Document types</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||
</svg><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.file-tasks">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
@@ -160,14 +163,14 @@
|
||||
</svg><span> <ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" tourAnchor="tour.settings">
|
||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
|
@@ -26,13 +26,17 @@ import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
templateUrl: './app-frame.component.html',
|
||||
styleUrls: ['./app-frame.component.scss'],
|
||||
})
|
||||
export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
||||
export class AppFrameComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, ComponentCanDeactivate
|
||||
{
|
||||
constructor(
|
||||
public router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -44,7 +48,9 @@ export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { interval, Subject, switchMap, take } from 'rxjs'
|
||||
import { interval, Subject, take } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog',
|
||||
|
@@ -5,10 +5,16 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
||||
|
||||
<div *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-correspondent-edit-dialog',
|
||||
@@ -12,8 +13,12 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||
})
|
||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: CorrespondentService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -30,6 +35,7 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -6,10 +6,16 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
</div>
|
||||
|
||||
<div *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-type-edit-dialog',
|
||||
@@ -12,8 +13,12 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||
})
|
||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: DocumentTypeService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -30,6 +35,7 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -2,19 +2,31 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Observable } from 'rxjs'
|
||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||
import {
|
||||
MATCHING_ALGORITHMS,
|
||||
MATCH_AUTO,
|
||||
MATCH_NONE,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
@Directive()
|
||||
export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
implements OnInit
|
||||
export abstract class EditDialogComponent<
|
||||
T extends ObjectWithPermissions | ObjectWithId
|
||||
> implements OnInit
|
||||
{
|
||||
constructor(
|
||||
private service: AbstractPaperlessService<T>,
|
||||
private activeModal: NgbActiveModal
|
||||
protected service: AbstractPaperlessService<T>,
|
||||
private activeModal: NgbActiveModal,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
users: PaperlessUser[]
|
||||
|
||||
@Input()
|
||||
dialogMode: string = 'create'
|
||||
|
||||
@@ -36,6 +48,14 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.object != null) {
|
||||
if (this.object['permissions']) {
|
||||
this.object['set_permissions'] = this.object['permissions']
|
||||
}
|
||||
|
||||
this.object['permissions_form'] = {
|
||||
owner: (this.object as ObjectWithPermissions).owner,
|
||||
set_permissions: (this.object as ObjectWithPermissions).permissions,
|
||||
}
|
||||
this.objectForm.patchValue(this.object)
|
||||
}
|
||||
|
||||
@@ -43,6 +63,8 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
setTimeout(() => {
|
||||
this.closeEnabled = true
|
||||
})
|
||||
|
||||
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -73,14 +95,24 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
}
|
||||
|
||||
get patternRequired(): boolean {
|
||||
return this.objectForm?.value.matching_algorithm !== MATCH_AUTO
|
||||
return (
|
||||
this.objectForm?.value.matching_algorithm !== MATCH_AUTO &&
|
||||
this.objectForm?.value.matching_algorithm !== MATCH_NONE
|
||||
)
|
||||
}
|
||||
|
||||
save() {
|
||||
var newObject = Object.assign(
|
||||
Object.assign({}, this.object),
|
||||
this.objectForm.value
|
||||
)
|
||||
this.error = null
|
||||
const formValues = Object.assign({}, this.objectForm.value)
|
||||
const permissionsObject: PermissionsFormObject =
|
||||
this.objectForm.get('permissions_form')?.value
|
||||
if (permissionsObject) {
|
||||
formValues.owner = permissionsObject.owner
|
||||
formValues.set_permissions = permissionsObject.set_permissions
|
||||
delete formValues.permissions_form
|
||||
}
|
||||
|
||||
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||
var serverResponse: Observable<T>
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
@@ -100,6 +132,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||
error: (error) => {
|
||||
this.error = error.error
|
||||
this.networkActive = false
|
||||
this.succeeded.next(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></app-permissions-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,37 @@
|
||||
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 { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-group-edit-dialog',
|
||||
templateUrl: './group-edit-dialog.component.html',
|
||||
styleUrls: ['./group-edit-dialog.component.scss'],
|
||||
})
|
||||
export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup> {
|
||||
constructor(
|
||||
service: GroupService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new user group`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit user group`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
permissions: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
@@ -15,11 +15,22 @@
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||
<app-input-check i18n-title title="Password is token" i18n-hint hint="Check if the password above is a token used for authentication" formControlName="is_token" [error]="error?.is_token"></app-input-check>
|
||||
<app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="m-0 me-2">
|
||||
<ngb-alert #testResultAlert *ngIf="testResult" [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive">
|
||||
<ng-container *ngIf="testActive">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span class="visually-hidden mr-1" i18n>Loading...</span>
|
||||
</ng-container>
|
||||
<ng-container i18n>Test</ng-container>
|
||||
</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>
|
||||
</div>
|
||||
|
@@ -0,0 +1,4 @@
|
||||
::ng-deep .alert-dismissible .btn-close {
|
||||
padding-top: 0.75rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import {
|
||||
IMAPSecurity,
|
||||
PaperlessMailAccount,
|
||||
} from 'src/app/data/paperless-mail-account'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
const IMAP_SECURITY_OPTIONS = [
|
||||
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
||||
@@ -20,8 +21,18 @@ const IMAP_SECURITY_OPTIONS = [
|
||||
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||
})
|
||||
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||
constructor(service: MailAccountService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
testActive: boolean = false
|
||||
testResult: string
|
||||
alertTimeout
|
||||
|
||||
@ViewChild('testResultAlert', { static: false }) testResultAlert: NgbAlert
|
||||
|
||||
constructor(
|
||||
service: MailAccountService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -40,6 +51,7 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
|
||||
imap_security: new FormControl(IMAPSecurity.SSL),
|
||||
username: new FormControl(null),
|
||||
password: new FormControl(null),
|
||||
is_token: new FormControl(false),
|
||||
character_set: new FormControl('UTF-8'),
|
||||
})
|
||||
}
|
||||
@@ -47,4 +59,33 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
|
||||
get imapSecurityOptions() {
|
||||
return IMAP_SECURITY_OPTIONS
|
||||
}
|
||||
|
||||
test() {
|
||||
this.testActive = true
|
||||
this.testResult = null
|
||||
clearTimeout(this.alertTimeout)
|
||||
const mailService = this.service as MailAccountService
|
||||
const newObject = Object.assign(
|
||||
Object.assign({}, this.object),
|
||||
this.objectForm.value
|
||||
)
|
||||
mailService.test(newObject).subscribe({
|
||||
next: (result: { success: boolean }) => {
|
||||
this.testActive = false
|
||||
this.testResult = result.success ? 'success' : 'danger'
|
||||
this.alertTimeout = setTimeout(() => this.testResultAlert.close(), 5000)
|
||||
},
|
||||
error: (e) => {
|
||||
this.testActive = false
|
||||
this.testResult = 'danger'
|
||||
this.alertTimeout = setTimeout(() => this.testResultAlert.close(), 5000)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
get testResultMessage() {
|
||||
return this.testResult === 'success'
|
||||
? $localize`Successfully connected to the mail server`
|
||||
: $localize`Unable to connect to the mail server`
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@
|
||||
<div class="col">
|
||||
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p>
|
||||
<app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text>
|
||||
<app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text>
|
||||
|
@@ -18,6 +18,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
const ATTACHMENT_TYPE_OPTIONS = [
|
||||
{
|
||||
@@ -113,9 +114,10 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
|
||||
activeModal: NgbActiveModal,
|
||||
accountService: MailAccountService,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService
|
||||
documentTypeService: DocumentTypeService,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal)
|
||||
super(service, activeModal, userService)
|
||||
|
||||
accountService
|
||||
.listAll()
|
||||
@@ -147,6 +149,7 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
|
||||
account: new FormControl(null),
|
||||
folder: new FormControl('INBOX'),
|
||||
filter_from: new FormControl(null),
|
||||
filter_to: new FormControl(null),
|
||||
filter_subject: new FormControl(null),
|
||||
filter_body: new FormControl(null),
|
||||
filter_attachment_filename: new FormControl(null),
|
||||
|
@@ -6,16 +6,16 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p *ngIf="this.dialogMode === 'edit'" i18n>
|
||||
<em>Note that editing a path does not apply changes to stored files until you have run the 'document_renamer' utility. See the <a target="_blank" href="https://docs.paperless-ngx.com/administration/#renamer">documentation</a>.</em>
|
||||
</p>
|
||||
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></app-input-text>
|
||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
|
||||
<div *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-storage-path-edit-dialog',
|
||||
@@ -12,8 +13,12 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||
})
|
||||
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||
constructor(service: StoragePathService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: StoragePathService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
get pathHint() {
|
||||
@@ -41,6 +46,7 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,11 @@
|
||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
|
||||
<div *appIfOwner="object">
|
||||
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
@@ -6,6 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { randomColor } from 'src/app/utils/color'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-edit-dialog',
|
||||
@@ -13,8 +14,12 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||
})
|
||||
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||
constructor(service: TagService, activeModal: NgbActiveModal) {
|
||||
super(service, activeModal)
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService
|
||||
) {
|
||||
super(service, activeModal, userService)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -33,6 +38,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
permissions_form: new FormControl(null),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||
<app-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></app-input-text>
|
||||
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||
<app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text>
|
||||
<app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active">
|
||||
<label class="form-check-label" for="is_active" i18n>Active</label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser" (change)="onToggleSuperUser()">
|
||||
<label class="form-check-label" for="is_superuser"><ng-container i18n>Superuser</ng-container> <small class="form-text text-muted ms-1" i18n>(Grants all permissions and can view objects)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions" [inheritedPermissions]="inheritedPermissions"></app-permissions-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,87 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-edit-dialog',
|
||||
templateUrl: './user-edit-dialog.component.html',
|
||||
styleUrls: ['./user-edit-dialog.component.scss'],
|
||||
})
|
||||
export class UserEditDialogComponent
|
||||
extends EditDialogComponent<PaperlessUser>
|
||||
implements OnInit
|
||||
{
|
||||
groups: PaperlessGroup[]
|
||||
passwordIsSet: boolean = false
|
||||
|
||||
constructor(
|
||||
service: UserService,
|
||||
activeModal: NgbActiveModal,
|
||||
groupsService: GroupService
|
||||
) {
|
||||
super(service, activeModal, service)
|
||||
|
||||
groupsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.groups = result.results))
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.onToggleSuperUser()
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new user account`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit user account`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
username: new FormControl(''),
|
||||
email: new FormControl(''),
|
||||
password: new FormControl(null),
|
||||
first_name: new FormControl(''),
|
||||
last_name: new FormControl(''),
|
||||
is_active: new FormControl(true),
|
||||
is_superuser: new FormControl(false),
|
||||
groups: new FormControl([]),
|
||||
user_permissions: new FormControl([]),
|
||||
})
|
||||
}
|
||||
|
||||
onToggleSuperUser() {
|
||||
if (this.objectForm.get('is_superuser').value) {
|
||||
this.objectForm.get('user_permissions').disable()
|
||||
} else {
|
||||
this.objectForm.get('user_permissions').enable()
|
||||
}
|
||||
}
|
||||
|
||||
get inheritedPermissions(): string[] {
|
||||
const groupsVal: Array<number> = this.objectForm.get('groups').value
|
||||
|
||||
if (!groupsVal) return []
|
||||
else
|
||||
return groupsVal.flatMap(
|
||||
(id) => this.groups.find((g) => g.id == id)?.permissions
|
||||
)
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.passwordIsSet =
|
||||
this.objectForm.get('password').value?.toString().replaceAll('*', '')
|
||||
.length > 0
|
||||
super.save()
|
||||
}
|
||||
}
|
@@ -1,21 +1,29 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
||||
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
||||
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||
<app-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||
</ng-container>
|
||||
</button>
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<div *ngIf="!editing && multiple" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs flex-fill">
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label>
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label>
|
||||
<div *ngIf="!editing && manyToOne" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
|
||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!editing && !manyToOne" class="list-group-item d-flex">
|
||||
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
|
||||
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
|
||||
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude">
|
||||
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
@@ -25,16 +33,16 @@
|
||||
</div>
|
||||
<div *ngIf="selectionModel.items" class="items">
|
||||
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)"></app-toggleable-dropdown-button>
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" [disabled]="disabled"></app-toggleable-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty">
|
||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
</svg>
|
||||
</button>
|
||||
<div *ngIf="!editing && multiple" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||
<small i18n>Click again to exclude items.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -11,18 +11,32 @@ import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { Subject } from 'rxjs'
|
||||
import { SelectionDataItem } from 'src/app/services/rest/document.service'
|
||||
|
||||
export interface ChangedItems {
|
||||
itemsToAdd: MatchingModel[]
|
||||
itemsToRemove: MatchingModel[]
|
||||
}
|
||||
|
||||
export enum LogicalOperator {
|
||||
And = 'and',
|
||||
Or = 'or',
|
||||
}
|
||||
|
||||
export enum Intersection {
|
||||
Include = 'include',
|
||||
Exclude = 'exclude',
|
||||
}
|
||||
|
||||
export class FilterableDropdownSelectionModel {
|
||||
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||
|
||||
multiple = false
|
||||
private _logicalOperator = 'and'
|
||||
temporaryLogicalOperator = this._logicalOperator
|
||||
manyToOne = false
|
||||
singleSelect = false
|
||||
private _logicalOperator: LogicalOperator = LogicalOperator.And
|
||||
temporaryLogicalOperator: LogicalOperator = this._logicalOperator
|
||||
private _intersection: Intersection = Intersection.Include
|
||||
temporaryIntersection: Intersection = this._intersection
|
||||
|
||||
items: MatchingModel[] = []
|
||||
|
||||
@@ -85,7 +99,30 @@ export class FilterableDropdownSelectionModel {
|
||||
(state != ToggleableItemState.Selected &&
|
||||
state != ToggleableItemState.Excluded)
|
||||
) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||
if (this.manyToOne || this.singleSelect) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||
|
||||
if (this.singleSelect) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let newState =
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
if (!id) newState = ToggleableItemState.Selected
|
||||
if (
|
||||
state == ToggleableItemState.Excluded &&
|
||||
this.intersection == Intersection.Exclude
|
||||
) {
|
||||
newState = ToggleableItemState.NotSelected
|
||||
}
|
||||
this.temporarySelectionStates.set(id, newState)
|
||||
}
|
||||
} else if (
|
||||
state == ToggleableItemState.Selected ||
|
||||
state == ToggleableItemState.Excluded
|
||||
@@ -93,14 +130,6 @@ export class FilterableDropdownSelectionModel {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
if (!this.multiple) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key) {
|
||||
@@ -118,19 +147,36 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
exclude(id: number, fireEvent: boolean = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (state == null || state != ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
||||
} else if (state == ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
||||
this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
|
||||
? LogicalOperator.And
|
||||
: LogicalOperator.Or
|
||||
|
||||
if (!this.multiple) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
if (this.manyToOne || this.singleSelect) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||
|
||||
if (this.singleSelect) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let newState =
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
if (
|
||||
state == ToggleableItemState.Selected &&
|
||||
this.intersection == Intersection.Include
|
||||
) {
|
||||
newState = ToggleableItemState.NotSelected
|
||||
}
|
||||
this.temporarySelectionStates.set(id, newState)
|
||||
}
|
||||
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
if (fireEvent) {
|
||||
@@ -142,11 +188,11 @@ export class FilterableDropdownSelectionModel {
|
||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||
}
|
||||
|
||||
get logicalOperator(): string {
|
||||
get logicalOperator(): LogicalOperator {
|
||||
return this.temporaryLogicalOperator
|
||||
}
|
||||
|
||||
set logicalOperator(operator: string) {
|
||||
set logicalOperator(operator: LogicalOperator) {
|
||||
this.temporaryLogicalOperator = operator
|
||||
}
|
||||
|
||||
@@ -154,6 +200,26 @@ export class FilterableDropdownSelectionModel {
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
get intersection(): Intersection {
|
||||
return this.temporaryIntersection
|
||||
}
|
||||
|
||||
set intersection(intersection: Intersection) {
|
||||
this.temporaryIntersection = intersection
|
||||
}
|
||||
|
||||
toggleIntersection() {
|
||||
if (this.temporarySelectionStates.size === 0) return
|
||||
let newState =
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
this.temporarySelectionStates.forEach((state, key) => {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
})
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
return (
|
||||
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||
@@ -170,7 +236,8 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
clear(fireEvent = true) {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
@@ -193,6 +260,8 @@ export class FilterableDropdownSelectionModel {
|
||||
return true
|
||||
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
||||
return true
|
||||
} else if (this.temporaryIntersection !== this._intersection) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@@ -216,13 +285,18 @@ export class FilterableDropdownSelectionModel {
|
||||
this.selectionStates.set(key, value)
|
||||
})
|
||||
this._logicalOperator = this.temporaryLogicalOperator
|
||||
this._intersection = this.temporaryIntersection
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(complete: boolean = false) {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.selectionStates.forEach((value, key) => {
|
||||
this.temporarySelectionStates.set(key, value)
|
||||
})
|
||||
if (complete) {
|
||||
this.selectionStates.clear()
|
||||
} else {
|
||||
this.selectionStates.forEach((value, key) => {
|
||||
this.temporarySelectionStates.set(key, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
diff(): ChangedItems {
|
||||
@@ -268,14 +342,16 @@ export class FilterableDropdownComponent {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel = new FilterableDropdownSelectionModel()
|
||||
_selectionModel: FilterableDropdownSelectionModel =
|
||||
new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
model.items = this.selectionModel.items
|
||||
model.multiple = this.selectionModel.multiple
|
||||
model.manyToOne = this.selectionModel.manyToOne
|
||||
model.singleSelect = this.editing && !this.selectionModel.manyToOne
|
||||
}
|
||||
model.changed.subscribe((updatedModel) => {
|
||||
this.selectionModelChange.next(updatedModel)
|
||||
@@ -291,12 +367,12 @@ export class FilterableDropdownComponent {
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set multiple(value: boolean) {
|
||||
this.selectionModel.multiple = value
|
||||
set manyToOne(manyToOne: boolean) {
|
||||
this.selectionModel.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
get multiple() {
|
||||
return this.selectionModel.multiple
|
||||
get manyToOne() {
|
||||
return this.selectionModel.manyToOne
|
||||
}
|
||||
|
||||
@Input()
|
||||
@@ -317,23 +393,38 @@ export class FilterableDropdownComponent {
|
||||
@Input()
|
||||
applyOnClose = false
|
||||
|
||||
@Input()
|
||||
disabled = false
|
||||
|
||||
@Output()
|
||||
apply = new EventEmitter<ChangedItems>()
|
||||
|
||||
@Output()
|
||||
opened = new EventEmitter()
|
||||
|
||||
get operatorToggleEnabled(): boolean {
|
||||
return (
|
||||
this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
)
|
||||
get modifierToggleEnabled(): boolean {
|
||||
return this.manyToOne
|
||||
? this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
: !this.selectionModel.isNoneSelected()
|
||||
}
|
||||
|
||||
@Input()
|
||||
documentCounts: SelectionDataItem[]
|
||||
|
||||
get name(): string {
|
||||
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||
}
|
||||
|
||||
getUpdatedDocumentCount(id: number) {
|
||||
if (this.documentCounts) {
|
||||
return this.documentCounts.find((c) => c.id === id)?.document_count
|
||||
}
|
||||
}
|
||||
|
||||
modelIsDirty: boolean = false
|
||||
|
||||
constructor(private filterPipe: FilterPipe) {
|
||||
this.selectionModel = new FilterableDropdownSelectionModel()
|
||||
this.selectionModelChange.subscribe((updatedModel) => {
|
||||
this.modelIsDirty = updatedModel.isDirty()
|
||||
})
|
||||
@@ -355,6 +446,7 @@ export class FilterableDropdownComponent {
|
||||
}, 0)
|
||||
if (this.editing) {
|
||||
this.selectionModel.reset()
|
||||
this.modelIsDirty = false
|
||||
}
|
||||
this.opened.next(this)
|
||||
} else {
|
||||
@@ -386,7 +478,7 @@ export class FilterableDropdownComponent {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectionModel.reset()
|
||||
this.selectionModel.reset(true)
|
||||
this.selectionModelChange.emit(this.selectionModel)
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user