Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40c14c1545 | ||
|
|
52ab07c673 | ||
|
|
046d8456e2 | ||
|
|
3314c59828 | ||
|
|
2103a499eb | ||
|
|
7ab779e78a | ||
|
|
49390c9427 | ||
|
|
6f29d64325 | ||
|
|
065724befb | ||
|
|
e877beea4e | ||
|
|
3e848e6e0f | ||
|
|
2e5656e1ce | ||
|
|
438650bf17 | ||
|
|
7df0b621a5 | ||
|
|
4cd755f641 | ||
|
|
8597911d85 | ||
|
|
befb80bddf | ||
|
|
7af6983cab | ||
|
|
54e72d5b60 | ||
|
|
cf7422346a | ||
|
|
5b8c9ef5fc | ||
|
|
f56974f158 | ||
|
|
427508edf1 | ||
|
|
311b259cff | ||
|
|
fce7b03324 | ||
|
|
79956d6a7b | ||
|
|
978b072bff | ||
|
|
9c6f695dbf |
@@ -51,7 +51,7 @@ repos:
|
||||
- 'prettier-plugin-organize-imports@4.1.0'
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.3
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
@@ -38,7 +38,6 @@ ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
[lint.per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
||||
"src/documents/consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/management/commands/document_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/management/commands/document_exporter.py" = ["PTH"] # TODO Enable & remove
|
||||
@@ -51,8 +50,6 @@ ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
"src/documents/signals/handlers.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tasks.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_app_config.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_bulk_download.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_documents.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_classifier.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||
|
||||
+12
-24
@@ -127,32 +127,21 @@ RUN set -eux \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
|
||||
&& echo "Installing pre-built updates" \
|
||||
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Cleaning up image layer" \
|
||||
&& rm --force --verbose *.deb \
|
||||
@@ -232,12 +221,11 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.4-cp312-cp312-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_x86_64.whl \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.4-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_aarch64.whl \
|
||||
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl \
|
||||
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
|
||||
@@ -57,8 +57,8 @@ tqdm = "*"
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=6.0"
|
||||
whitenoise = "~=6.8"
|
||||
whoosh = "~=2.7"
|
||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
whoosh-reloaded = "*"
|
||||
zxing-cpp = "*"
|
||||
|
||||
|
||||
[dev-packages]
|
||||
|
||||
Generated
+50
-47
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3806c1dbfde8e9383e748c106c217170d6dcdbb8b95d573030b2294dab32d462"
|
||||
"sha256": "f1567fb098714a4bb2999bcf04c7d8b3ad6fb04dced2f971f95c21fe8792a878"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -193,6 +193,14 @@
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"cached-property": {
|
||||
"hashes": [
|
||||
"sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641",
|
||||
"sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"celery": {
|
||||
"extras": [
|
||||
"redis"
|
||||
@@ -589,12 +597,12 @@
|
||||
},
|
||||
"django-soft-delete": {
|
||||
"hashes": [
|
||||
"sha256:cc40398ccd869c75a6d6ba7f526e16c4afe2b0c0811c213a318d96bb4c58a787",
|
||||
"sha256:fdaf2788d404930557f1300ce40bbd764f6938775a35a3175c66fe7778666093"
|
||||
"sha256:603a29e82bbb7a5bada69f2754fad225ccd8cd7f485320ec06d0fc4e9dfddcf0",
|
||||
"sha256:d2f9db449a4f008e9786f82fa4bafbe4075f7a0b3284844735007e988b2a4df6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.16"
|
||||
"version": "==1.0.18"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
@@ -2264,7 +2272,7 @@
|
||||
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
|
||||
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==4.12.2"
|
||||
},
|
||||
"tzdata": {
|
||||
@@ -2565,14 +2573,13 @@
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==6.8.2"
|
||||
},
|
||||
"whoosh": {
|
||||
"whoosh-reloaded": {
|
||||
"hashes": [
|
||||
"sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83",
|
||||
"sha256:aa39c3c3426e3fd107dcb4bde64ca1e276a65a889d9085a6e4b54ba82420a852",
|
||||
"sha256:e0857375f63e9041e03fedd5b7541f97cf78917ac1b6b06c1fcc9b45375dda69"
|
||||
"sha256:2ab6aeeafb359fbff4beb3c704b960fd88240354f3363f1c5bdb5c2325cae80e",
|
||||
"sha256:39ed7dfbd1fec97af33933107bdf78110728375ed0f2abb25dec6dbfdcb279d8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.4"
|
||||
"version": "==2.7.5"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
@@ -2791,7 +2798,6 @@
|
||||
"sha256:fbd5b253ad0f8823c5c104feaaa19acab95c217cb924b012d55ff339c42b3583",
|
||||
"sha256:fd3f175f7b57cfbdea56afdb5335eaebaadeebc06e20a087d9aa3f99637c4aa5"
|
||||
],
|
||||
"markers": "platform_machine == 'x86_64'",
|
||||
"version": "==2.3.0"
|
||||
}
|
||||
},
|
||||
@@ -2838,19 +2844,19 @@
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b",
|
||||
"sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"
|
||||
"sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d",
|
||||
"sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.16.0"
|
||||
"version": "==2.17.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56",
|
||||
"sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"
|
||||
"sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
|
||||
"sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2024.12.14"
|
||||
"version": "==2025.1.31"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@@ -3303,7 +3309,6 @@
|
||||
"sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb",
|
||||
"sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.5"
|
||||
},
|
||||
@@ -3416,12 +3421,12 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825",
|
||||
"sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385"
|
||||
"sha256:71d90dbd63b393ad11a4d90151dfe3dcbfcd802c0f29ce80bebd9bbac6abc753",
|
||||
"sha256:a3de1c5d4c745f10afa78b1a02f917b9dce0808fb206adc0f5bb48b58c1ca21f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==9.5.50"
|
||||
"version": "==9.6.2"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
@@ -3659,11 +3664,11 @@
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:637951cbfbe9874ba28134fb3ce4b8bcadd6aca89ac4998ec29dcbafd554ae08",
|
||||
"sha256:b65801996a0cd4f42a3110810c306c45b7313c09b0610a6f773730f2a9e3c96b"
|
||||
"sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9",
|
||||
"sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.14.1"
|
||||
"version": "==10.14.3"
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
@@ -3758,8 +3763,7 @@
|
||||
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
|
||||
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.9.0.post0"
|
||||
},
|
||||
"pywavelets": {
|
||||
@@ -3983,28 +3987,28 @@
|
||||
},
|
||||
"ruff": {
|
||||
"hashes": [
|
||||
"sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2",
|
||||
"sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4",
|
||||
"sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439",
|
||||
"sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730",
|
||||
"sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4",
|
||||
"sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5",
|
||||
"sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624",
|
||||
"sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b",
|
||||
"sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a",
|
||||
"sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b",
|
||||
"sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5",
|
||||
"sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4",
|
||||
"sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c",
|
||||
"sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519",
|
||||
"sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1",
|
||||
"sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4",
|
||||
"sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6",
|
||||
"sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"
|
||||
"sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e",
|
||||
"sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214",
|
||||
"sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137",
|
||||
"sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c",
|
||||
"sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b",
|
||||
"sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b",
|
||||
"sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41",
|
||||
"sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706",
|
||||
"sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7",
|
||||
"sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf",
|
||||
"sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec",
|
||||
"sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6",
|
||||
"sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231",
|
||||
"sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0",
|
||||
"sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402",
|
||||
"sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e",
|
||||
"sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a",
|
||||
"sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.9.3"
|
||||
"version": "==0.9.4"
|
||||
},
|
||||
"scipy": {
|
||||
"hashes": [
|
||||
@@ -4073,7 +4077,7 @@
|
||||
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
|
||||
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.17.0"
|
||||
},
|
||||
"sniffio": {
|
||||
@@ -4206,7 +4210,6 @@
|
||||
"sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c",
|
||||
"sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
|
||||
@@ -1073,8 +1073,6 @@ or hidden folders some tools use to store data.
|
||||
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.
|
||||
|
||||
zxing is not available on all platforms.
|
||||
|
||||
#### [`PAPERLESS_PRE_CONSUME_SCRIPT=<filename>`](#PAPERLESS_PRE_CONSUME_SCRIPT) {#PAPERLESS_PRE_CONSUME_SCRIPT}
|
||||
|
||||
: After some initial validation, Paperless can trigger an arbitrary
|
||||
|
||||
+606
-565
File diff suppressed because it is too large
Load Diff
Generated
+1836
-1897
File diff suppressed because it is too large
Load Diff
+30
-29
@@ -11,17 +11,17 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.0.2",
|
||||
"@angular/common": "~19.0.3",
|
||||
"@angular/compiler": "~19.0.3",
|
||||
"@angular/core": "~19.0.3",
|
||||
"@angular/forms": "~19.0.3",
|
||||
"@angular/localize": "~19.0.3",
|
||||
"@angular/platform-browser": "~19.0.3",
|
||||
"@angular/platform-browser-dynamic": "~19.0.3",
|
||||
"@angular/router": "~19.0.3",
|
||||
"@angular/cdk": "^19.1.2",
|
||||
"@angular/common": "~19.1.4",
|
||||
"@angular/compiler": "~19.1.4",
|
||||
"@angular/core": "~19.1.4",
|
||||
"@angular/forms": "~19.1.4",
|
||||
"@angular/localize": "~19.1.4",
|
||||
"@angular/platform-browser": "~19.1.4",
|
||||
"@angular/platform-browser-dynamic": "~19.1.4",
|
||||
"@angular/router": "~19.1.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||
"@ng-select/ng-select": "^14.1.0",
|
||||
"@ng-select/ng-select": "^14.2.0",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
@@ -30,36 +30,37 @@
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^19.0.0",
|
||||
"ngx-cookie-service": "^19.1.0",
|
||||
"ngx-device-detector": "^9.0.0",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.8.1",
|
||||
"utif": "^3.1.0",
|
||||
"uuid": "^11.0.2",
|
||||
"uuid": "^11.0.5",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^19.0.0-beta.0",
|
||||
"@angular-builders/jest": "^19.0.0-beta.1",
|
||||
"@angular-builders/custom-webpack": "^19.0.0",
|
||||
"@angular-builders/jest": "^19.0.0",
|
||||
"@angular-devkit/build-angular": "^19.0.4",
|
||||
"@angular-devkit/core": "^19.0.4",
|
||||
"@angular-devkit/schematics": "^19.0.4",
|
||||
"@angular-eslint/builder": "19.0.0",
|
||||
"@angular-eslint/eslint-plugin": "19.0.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.0",
|
||||
"@angular-eslint/schematics": "19.0.0",
|
||||
"@angular-eslint/template-parser": "19.0.0",
|
||||
"@angular/cli": "~19.0.4",
|
||||
"@angular/compiler-cli": "~19.0.3",
|
||||
"@codecov/webpack-plugin": "^1.2.1",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@angular-devkit/core": "^19.1.5",
|
||||
"@angular-devkit/schematics": "^19.1.5",
|
||||
"@angular-eslint/builder": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.2",
|
||||
"@angular-eslint/schematics": "19.0.2",
|
||||
"@angular-eslint/template-parser": "19.0.2",
|
||||
"@angular/cli": "~19.1.5",
|
||||
"@angular/compiler-cli": "~19.1.4",
|
||||
"@codecov/webpack-plugin": "^1.8.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.8.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
||||
"@typescript-eslint/parser": "^8.12.2",
|
||||
"@types/node": "^22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@typescript-eslint/utils": "^8.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint": "^9.19.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^14.4.2",
|
||||
|
||||
@@ -100,6 +100,15 @@ Object.defineProperty(navigator, 'clipboard', {
|
||||
},
|
||||
})
|
||||
Object.defineProperty(navigator, 'canShare', { value: () => true })
|
||||
if (!navigator.share) {
|
||||
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
||||
}
|
||||
if (!URL.createObjectURL) {
|
||||
Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() })
|
||||
}
|
||||
if (!URL.revokeObjectURL) {
|
||||
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
||||
}
|
||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
|
||||
@@ -18,20 +18,20 @@ import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
import { PermissionsGuard } from './guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from './services/consumer-status.service'
|
||||
import { HotKeyService } from './services/hot-key.service'
|
||||
import { PermissionsService } from './services/permissions.service'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { Toast, ToastService } from './services/toast.service'
|
||||
import {
|
||||
FileStatus,
|
||||
WebsocketStatusService,
|
||||
} from './services/websocket-status.service'
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent
|
||||
let fixture: ComponentFixture<AppComponent>
|
||||
let tourService: TourService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let permissionsService: PermissionsService
|
||||
let toastService: ToastService
|
||||
let router: Router
|
||||
@@ -59,7 +59,7 @@ describe('AppComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
tourService = TestBed.inject(TourService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
@@ -90,7 +90,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
const status = new FileStatus()
|
||||
@@ -109,7 +109,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
@@ -122,7 +122,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.spyOn(websocketStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
@@ -136,7 +136,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.spyOn(websocketStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
@@ -148,7 +148,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFailed')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
|
||||
@@ -5,7 +5,7 @@ import { first, Subscription } from 'rxjs'
|
||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||
import { SETTINGS_KEYS } from './data/ui-settings'
|
||||
import { ConsumerStatusService } from './services/consumer-status.service'
|
||||
import { ComponentRouterService } from './services/component-router.service'
|
||||
import { HotKeyService } from './services/hot-key.service'
|
||||
import {
|
||||
PermissionAction,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { TasksService } from './services/tasks.service'
|
||||
import { ToastService } from './services/toast.service'
|
||||
import { WebsocketStatusService } from './services/websocket-status.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-root',
|
||||
@@ -34,14 +35,15 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private settings: SettingsService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
private toastService: ToastService,
|
||||
private router: Router,
|
||||
private tasksService: TasksService,
|
||||
public tourService: TourService,
|
||||
private renderer: Renderer2,
|
||||
private permissionsService: PermissionsService,
|
||||
private hotKeyService: HotKeyService
|
||||
private hotKeyService: HotKeyService,
|
||||
private componentRouterService: ComponentRouterService
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
|
||||
@@ -49,7 +51,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.consumerStatusService.disconnect()
|
||||
this.websocketStatusService.disconnect()
|
||||
if (this.successSubscription) {
|
||||
this.successSubscription.unsubscribe()
|
||||
}
|
||||
@@ -74,9 +76,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.consumerStatusService.connect()
|
||||
this.websocketStatusService.connect()
|
||||
|
||||
this.successSubscription = this.consumerStatusService
|
||||
this.successSubscription = this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
@@ -106,7 +108,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
|
||||
this.failedSubscription = this.consumerStatusService
|
||||
this.failedSubscription = this.websocketStatusService
|
||||
.onDocumentConsumptionFailed()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
@@ -119,7 +121,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
|
||||
this.newDocumentSubscription = this.consumerStatusService
|
||||
this.newDocumentSubscription = this.websocketStatusService
|
||||
.onDocumentDetected()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row">
|
||||
<div class="col-xl-6 pe-xl-5">
|
||||
<h4 i18n>Appearance</h4>
|
||||
<h5 i18n>Appearance</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Display language</span>
|
||||
@@ -154,28 +154,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Document editing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6 ps-xl-5">
|
||||
<h4 class="mt-4 mt-md-0" id="update-checking" i18n>Update checking</h4>
|
||||
<h5 class="mt-3" id="update-checking" i18n>Update checking</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col d-flex flex-row align-items-start">
|
||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||
@@ -193,7 +172,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||
<h5 class="mt-3" i18n>Saved Views</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xl-6 ps-xl-5">
|
||||
<h5 class="mt-3 mt-md-0" i18n>Document editing</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
<span i18n>Default zoom:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
||||
<option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option>
|
||||
<option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option>
|
||||
</select>
|
||||
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Notes</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Bulk editing</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs"></pngx-input-check>
|
||||
@@ -201,7 +229,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Global search</h4>
|
||||
<h5 class="mt-3" i18n>Global search</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
|
||||
@@ -224,19 +252,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Saved Views</h4>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Notes</h4>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -247,7 +262,7 @@
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Default Permissions</h4>
|
||||
<h5 i18n>Default Permissions</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
@@ -329,7 +344,7 @@
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Document processing</h4>
|
||||
<h5 i18n>Document processing</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('SettingsComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(storeSpy).toHaveBeenCalled()
|
||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||
expect(setSpy).toHaveBeenCalledTimes(28)
|
||||
expect(setSpy).toHaveBeenCalledTimes(29)
|
||||
|
||||
// succeed
|
||||
storeSpy.mockReturnValueOnce(of(true))
|
||||
|
||||
@@ -63,6 +63,7 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||
import { ZoomSetting } from '../../document-detail/document-detail.component'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
enum SettingsNavIDs {
|
||||
@@ -125,6 +126,7 @@ export class SettingsComponent
|
||||
defaultPermsEditUsers: new FormControl(null),
|
||||
defaultPermsEditGroups: new FormControl(null),
|
||||
useNativePdfViewer: new FormControl(null),
|
||||
pdfViewerDefaultZoom: new FormControl(null),
|
||||
documentEditingRemoveInboxTags: new FormControl(null),
|
||||
documentEditingOverlayThumbnail: new FormControl(null),
|
||||
searchDbOnly: new FormControl(null),
|
||||
@@ -154,6 +156,8 @@ export class SettingsComponent
|
||||
|
||||
public readonly GlobalSearchType = GlobalSearchType
|
||||
|
||||
public readonly ZoomSetting = ZoomSetting
|
||||
|
||||
get systemStatusHasErrors(): boolean {
|
||||
return (
|
||||
this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
|
||||
@@ -276,6 +280,9 @@ export class SettingsComponent
|
||||
useNativePdfViewer: this.settings.get(
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
|
||||
),
|
||||
pdfViewerDefaultZoom: this.settings.get(
|
||||
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING
|
||||
),
|
||||
displayLanguage: this.settings.getLanguage(),
|
||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||
@@ -435,6 +442,10 @@ export class SettingsComponent
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER,
|
||||
this.settingsForm.value.useNativePdfViewer
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||
this.settingsForm.value.pdfViewerDefaultZoom
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.DATE_LOCALE,
|
||||
this.settingsForm.value.dateLocale
|
||||
|
||||
@@ -86,12 +86,17 @@ export class TrashComponent
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.trashService.emptyTrash([document.id]).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Document deleted`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${document.title}" deleted`
|
||||
)
|
||||
modal.close()
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.showError($localize`Error deleting document`, err)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting document "${document.title}"`,
|
||||
err
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
})
|
||||
@@ -136,7 +141,7 @@ export class TrashComponent
|
||||
this.trashService.restoreDocuments([document.id]).subscribe({
|
||||
next: () => {
|
||||
this.toastService.show({
|
||||
content: $localize`Document restored`,
|
||||
content: $localize`Document "${document.title}" restored`,
|
||||
delay: 5000,
|
||||
actionName: $localize`Open document`,
|
||||
action: () => {
|
||||
@@ -146,7 +151,10 @@ export class TrashComponent
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.showError($localize`Error restoring document`, err)
|
||||
this.toastService.showError(
|
||||
$localize`Error restoring document "${document.title}"`,
|
||||
err
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
|
||||
})
|
||||
|
||||
it('should logout current user if password changed, after delay', fakeAsync(() => {
|
||||
@@ -178,7 +178,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteGroup(users[0])
|
||||
component.deleteGroup(groups[0])
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(groupService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
@@ -192,7 +192,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group "group1"')
|
||||
})
|
||||
|
||||
it('should get group name', () => {
|
||||
|
||||
@@ -129,13 +129,16 @@ export class UsersAndGroupsComponent
|
||||
this.usersService.delete(user).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted user`)
|
||||
this.toastService.showInfo($localize`Deleted user "${user.username}"`)
|
||||
this.usersService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting user.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting user "${user.username}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -179,13 +182,16 @@ export class UsersAndGroupsComponent
|
||||
this.groupsService.delete(group).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted group`)
|
||||
this.toastService.showInfo($localize`Deleted group "${group.name}"`)
|
||||
this.groupsService.listAll().subscribe((r) => {
|
||||
this.groups = r.results
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting group.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting group "${group.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
+7
-7
@@ -33,14 +33,14 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import {
|
||||
FileStatus,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { SavedViewWidgetComponent } from './saved-view-widget.component'
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
let component: SavedViewWidgetComponent
|
||||
let fixture: ComponentFixture<SavedViewWidgetComponent>
|
||||
let documentService: DocumentService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let router: Router
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
router = TestBed.inject(Router)
|
||||
fixture = TestBed.createComponent(SavedViewWidgetComponent)
|
||||
@@ -235,7 +235,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
it('should reload on document consumption finished', () => {
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
const reloadSpy = jest.spyOn(component, 'reload')
|
||||
component.ngOnInit()
|
||||
|
||||
+3
-3
@@ -42,7 +42,6 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
|
||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import {
|
||||
@@ -53,6 +52,7 @@ import {
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
|
||||
@Component({
|
||||
@@ -94,7 +94,7 @@ export class SavedViewWidgetComponent
|
||||
private documentService: DocumentService,
|
||||
private router: Router,
|
||||
private list: DocumentListViewService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public documentListViewService: DocumentListViewService,
|
||||
public permissionsService: PermissionsService,
|
||||
@@ -124,7 +124,7 @@ export class SavedViewWidgetComponent
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
|
||||
this.consumerStatusService
|
||||
this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
|
||||
+5
-5
@@ -12,9 +12,9 @@ import { routes } from 'src/app/app-routing.module'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { StatisticsWidgetComponent } from './statistics-widget.component'
|
||||
@@ -23,7 +23,7 @@ describe('StatisticsWidgetComponent', () => {
|
||||
let component: StatisticsWidgetComponent
|
||||
let fixture: ComponentFixture<StatisticsWidgetComponent>
|
||||
let httpTestingController: HttpTestingController
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -44,9 +44,9 @@ describe('StatisticsWidgetComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(StatisticsWidgetComponent)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component = fixture.componentInstance
|
||||
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ import { first, Subject, Subscription, takeUntil } from 'rxjs'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { FILTER_HAS_TAGS_ANY } from 'src/app/data/filter-rule-type'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
|
||||
@@ -51,7 +51,7 @@ export class StatisticsWidgetComponent
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketConnectionService: WebsocketStatusService,
|
||||
private documentListViewService: DocumentListViewService
|
||||
) {
|
||||
super()
|
||||
@@ -109,7 +109,7 @@ export class StatisticsWidgetComponent
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.subscription = this.consumerStatusService
|
||||
this.subscription = this.websocketConnectionService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe(() => {
|
||||
this.reload()
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@ import { NgbAlert, NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
import {
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
|
||||
|
||||
const FAILED_STATUSES = [new FileStatus()]
|
||||
@@ -42,7 +42,7 @@ const DEFAULT_STATUSES = [
|
||||
describe('UploadFileWidgetComponent', () => {
|
||||
let component: UploadFileWidgetComponent
|
||||
let fixture: ComponentFixture<UploadFileWidgetComponent>
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -65,7 +65,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
|
||||
fixture = TestBed.createComponent(UploadFileWidgetComponent)
|
||||
component = fixture.componentInstance
|
||||
@@ -91,14 +91,14 @@ describe('UploadFileWidgetComponent', () => {
|
||||
})
|
||||
|
||||
it('should generate stats summary', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
expect(component.getStatusSummary()).toEqual(
|
||||
'Processing: 6, Failed: 1, Added: 4'
|
||||
)
|
||||
})
|
||||
|
||||
it('should report an upload progress summary', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
expect(component.getTotalUploadProgress()).toEqual(0.75)
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
})
|
||||
|
||||
it('should enforce a maximum number of alerts', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
fixture.detectChanges()
|
||||
// 5 total, 1 hidden
|
||||
expect(fixture.debugElement.queryAll(By.directive(NgbAlert))).toHaveLength(
|
||||
@@ -131,19 +131,19 @@ describe('UploadFileWidgetComponent', () => {
|
||||
})
|
||||
|
||||
it('should allow dismissing an alert', () => {
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
|
||||
component.dismiss(new FileStatus())
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow dismissing completed alerts', fakeAsync(() => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
component.alertsExpanded = true
|
||||
fixture.detectChanges()
|
||||
jest
|
||||
.spyOn(component, 'getStatusCompleted')
|
||||
.mockImplementation(() => SUCCESS_STATUSES)
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
|
||||
component.dismissCompleted()
|
||||
tick(1000)
|
||||
fixture.detectChanges()
|
||||
|
||||
+19
-16
@@ -12,13 +12,13 @@ import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
import {
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
|
||||
const MAX_ALERTS = 5
|
||||
@@ -46,7 +46,7 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
@ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
|
||||
|
||||
constructor(
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
public settingsService: SettingsService
|
||||
) {
|
||||
@@ -54,13 +54,13 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
||||
return this.websocketStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
||||
}
|
||||
|
||||
getStatusSummary() {
|
||||
let strings = []
|
||||
let countUploadingAndProcessing =
|
||||
this.consumerStatusService.getConsumerStatusNotCompleted().length
|
||||
this.websocketStatusService.getConsumerStatusNotCompleted().length
|
||||
let countFailed = this.getStatusFailed().length
|
||||
let countSuccess = this.getStatusSuccess().length
|
||||
if (countUploadingAndProcessing > 0) {
|
||||
@@ -78,27 +78,30 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
getStatusHidden() {
|
||||
if (this.consumerStatusService.getConsumerStatus().length < MAX_ALERTS)
|
||||
if (this.websocketStatusService.getConsumerStatus().length < MAX_ALERTS)
|
||||
return []
|
||||
else return this.consumerStatusService.getConsumerStatus().slice(MAX_ALERTS)
|
||||
else
|
||||
return this.websocketStatusService.getConsumerStatus().slice(MAX_ALERTS)
|
||||
}
|
||||
|
||||
getStatusUploading() {
|
||||
return this.consumerStatusService.getConsumerStatus(
|
||||
return this.websocketStatusService.getConsumerStatus(
|
||||
FileStatusPhase.UPLOADING
|
||||
)
|
||||
}
|
||||
|
||||
getStatusFailed() {
|
||||
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
return this.websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
}
|
||||
|
||||
getStatusSuccess() {
|
||||
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
|
||||
return this.websocketStatusService.getConsumerStatus(
|
||||
FileStatusPhase.SUCCESS
|
||||
)
|
||||
}
|
||||
|
||||
getStatusCompleted() {
|
||||
return this.consumerStatusService.getConsumerStatusCompleted()
|
||||
return this.websocketStatusService.getConsumerStatusCompleted()
|
||||
}
|
||||
|
||||
getTotalUploadProgress() {
|
||||
@@ -134,12 +137,12 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
dismiss(status: FileStatus) {
|
||||
this.consumerStatusService.dismiss(status)
|
||||
this.websocketStatusService.dismiss(status)
|
||||
}
|
||||
|
||||
dismissCompleted() {
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
this.websocketStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
}
|
||||
<div class="input-group input-group-sm me-md-5 d-none d-md-flex">
|
||||
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
|
||||
<select class="form-select" (change)="onZoomSelect($event)">
|
||||
<select class="form-select" (change)="setZoom($event.target.value)">
|
||||
@for (setting of zoomSettings; track setting) {
|
||||
<option [value]="setting" [selected]="previewZoomSetting === setting">
|
||||
<option [value]="setting" [attr.selected]="isZoomSelected(setting) ? 'selected' : null">
|
||||
{{ getZoomSettingTitle(setting) }}
|
||||
</option>
|
||||
}
|
||||
@@ -25,15 +25,20 @@
|
||||
</button>
|
||||
|
||||
<div class="btn-group">
|
||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</a>
|
||||
<button (click)="download()" class="btn btn-sm btn-outline-primary" [disabled]="downloading">
|
||||
@if (downloading) {
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
} @else {
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs>
|
||||
}
|
||||
<span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</button>
|
||||
|
||||
@if (metadata?.has_archive_version) {
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" [disabled]="downloading" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
<button ngbDropdownItem (click)="download(true)" [disabled]="downloading" i18n>Download original</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -351,9 +356,9 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #previewContent>
|
||||
<div class="thumb-preview position-absolute pe-none" [class.fade]="previewLoaded">
|
||||
<div class="thumb-preview position-absolute pe-none text-center" [class.fade]="previewLoaded">
|
||||
@if (showThumbnailOverlay) {
|
||||
<img [src]="thumbUrl | safeUrl" class="" width="100%" height="auto" alt="Document loading..." i18n-alt />
|
||||
<img [src]="thumbUrl | safeUrl" class="mx-auto" [attr.width]="previewZoomScale === 'page-fit' ? 'auto' : '100%'" [attr.height]="previewZoomScale === 'page-fit' ? '100%' : 'auto'" alt="Document loading..." i18n-alt />
|
||||
}
|
||||
<div class="position-absolute top-0 start-0 m-2 p-2 d-flex align-items-center justify-content-center">
|
||||
<div>
|
||||
|
||||
@@ -85,5 +85,8 @@ textarea.rtl {
|
||||
|
||||
> img {
|
||||
filter: blur(1px);
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
@@ -45,6 +46,7 @@ import { Tag } from 'src/app/data/tag'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { ComponentRouterService } from 'src/app/services/component-router.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
@@ -60,7 +62,10 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import { DocumentDetailComponent } from './document-detail.component'
|
||||
import {
|
||||
DocumentDetailComponent,
|
||||
ZoomSetting,
|
||||
} from './document-detail.component'
|
||||
|
||||
const doc: Document = {
|
||||
id: 3,
|
||||
@@ -126,7 +131,9 @@ describe('DocumentDetailComponent', () => {
|
||||
let documentListViewService: DocumentListViewService
|
||||
let settingsService: SettingsService
|
||||
let customFieldsService: CustomFieldsService
|
||||
let deviceDetectorService: DeviceDetectorService
|
||||
let httpTestingController: HttpTestingController
|
||||
let componentRouterService: ComponentRouterService
|
||||
|
||||
let currentUserCan = true
|
||||
let currentUserHasObjectPermissions = true
|
||||
@@ -262,8 +269,10 @@ describe('DocumentDetailComponent', () => {
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 1 }
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
deviceDetectorService = TestBed.inject(DeviceDetectorService)
|
||||
fixture = TestBed.createComponent(DocumentDetailComponent)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
componentRouterService = TestBed.inject(ComponentRouterService)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
@@ -448,7 +457,9 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save(true)
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Document "Doc 3" saved successfully.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support save without close and show success toast', () => {
|
||||
@@ -461,7 +472,9 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Document "Doc 3" saved successfully.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show toast error on save if error occurs', () => {
|
||||
@@ -476,7 +489,10 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Error saving document', error)
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Error saving document "Doc 3"',
|
||||
error
|
||||
)
|
||||
})
|
||||
|
||||
it('should show error toast on save but close if user can no longer edit', () => {
|
||||
@@ -492,7 +508,9 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save(true)
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Document "Doc 3" saved successfully.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow save and next', () => {
|
||||
@@ -568,6 +586,16 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents'])
|
||||
})
|
||||
|
||||
it('should allow close and navigate to the last view if available', () => {
|
||||
initNormally()
|
||||
jest
|
||||
.spyOn(componentRouterService, 'getComponentURLBefore')
|
||||
.mockReturnValue('dashboard')
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.close()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['dashboard'])
|
||||
})
|
||||
|
||||
it('should allow close and navigate to documents by default', () => {
|
||||
initNormally()
|
||||
jest
|
||||
@@ -728,7 +756,7 @@ describe('DocumentDetailComponent', () => {
|
||||
|
||||
it('should support zoom controls', () => {
|
||||
initNormally()
|
||||
component.onZoomSelect({ target: { value: '1' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.One) // from select
|
||||
expect(component.previewZoomSetting).toEqual('1')
|
||||
component.increaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('1.5')
|
||||
@@ -736,18 +764,18 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(component.previewZoomSetting).toEqual('2')
|
||||
component.decreaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('1.5')
|
||||
component.onZoomSelect({ target: { value: '1' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.One) // from select
|
||||
component.decreaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('.75')
|
||||
|
||||
component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.PageFit) // from select
|
||||
expect(component.previewZoomScale).toEqual('page-fit')
|
||||
expect(component.previewZoomSetting).toEqual('1')
|
||||
component.increaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('1.5')
|
||||
expect(component.previewZoomScale).toEqual('page-width')
|
||||
|
||||
component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.PageFit) // from select
|
||||
expect(component.previewZoomScale).toEqual('page-fit')
|
||||
expect(component.previewZoomSetting).toEqual('1')
|
||||
component.decreaseZoom()
|
||||
@@ -755,6 +783,19 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(component.previewZoomScale).toEqual('page-width')
|
||||
})
|
||||
|
||||
it('should select correct zoom setting in dropdown', () => {
|
||||
initNormally()
|
||||
component.setZoom(ZoomSetting.PageFit)
|
||||
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeTruthy()
|
||||
expect(component.isZoomSelected(ZoomSetting.One)).toBeFalsy()
|
||||
component.setZoom(ZoomSetting.PageWidth)
|
||||
expect(component.isZoomSelected(ZoomSetting.One)).toBeTruthy()
|
||||
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy()
|
||||
component.setZoom(ZoomSetting.Quarter)
|
||||
expect(component.isZoomSelected(ZoomSetting.Quarter)).toBeTruthy()
|
||||
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support updating notes dynamically', () => {
|
||||
const notes = [
|
||||
{
|
||||
@@ -1255,4 +1296,38 @@ describe('DocumentDetailComponent', () => {
|
||||
.error(new ErrorEvent('failed'))
|
||||
expect(component.tiffError).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support download using share sheet on mobile, direct download otherwise', () => {
|
||||
const shareSpy = jest.spyOn(navigator, 'share')
|
||||
const createSpy = jest.spyOn(document, 'createElement')
|
||||
const urlRevokeSpy = jest.spyOn(URL, 'revokeObjectURL')
|
||||
initNormally()
|
||||
|
||||
// Mobile
|
||||
jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(false)
|
||||
component.download()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`)
|
||||
.error(new ProgressEvent('failed'))
|
||||
expect(shareSpy).not.toHaveBeenCalled()
|
||||
|
||||
component.download(true)
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}documents/${doc.id}/download/?original=true`
|
||||
)
|
||||
.flush(new ArrayBuffer(100))
|
||||
expect(shareSpy).toHaveBeenCalled()
|
||||
|
||||
// Desktop
|
||||
shareSpy.mockClear()
|
||||
jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(true)
|
||||
component.download()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`)
|
||||
.flush(new ArrayBuffer(100))
|
||||
expect(shareSpy).not.toHaveBeenCalled()
|
||||
expect(createSpy).toHaveBeenCalledWith('a')
|
||||
expect(urlRevokeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
@@ -59,6 +60,7 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { ComponentRouterService } from 'src/app/services/component-router.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -122,7 +124,7 @@ enum ContentRenderType {
|
||||
TIFF = 'tiff',
|
||||
}
|
||||
|
||||
enum ZoomSetting {
|
||||
export enum ZoomSetting {
|
||||
PageFit = 'page-fit',
|
||||
PageWidth = 'page-width',
|
||||
Quarter = '.25',
|
||||
@@ -194,8 +196,6 @@ export class DocumentDetailComponent
|
||||
previewUrl: string
|
||||
thumbUrl: string
|
||||
previewText: string
|
||||
downloadUrl: string
|
||||
downloadOriginalUrl: string
|
||||
previewLoaded: boolean = false
|
||||
tiffURL: string
|
||||
tiffError: string
|
||||
@@ -233,6 +233,9 @@ export class DocumentDetailComponent
|
||||
ogDate: Date
|
||||
|
||||
customFields: CustomField[]
|
||||
|
||||
public downloading: boolean = false
|
||||
|
||||
public readonly CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
public readonly ContentRenderType = ContentRenderType
|
||||
@@ -272,7 +275,9 @@ export class DocumentDetailComponent
|
||||
private userService: UserService,
|
||||
private customFieldsService: CustomFieldsService,
|
||||
private http: HttpClient,
|
||||
private hotKeyService: HotKeyService
|
||||
private hotKeyService: HotKeyService,
|
||||
private componentRouterService: ComponentRouterService,
|
||||
private deviceDetectorService: DeviceDetectorService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -323,6 +328,7 @@ export class DocumentDetailComponent
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING))
|
||||
this.documentForm.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
@@ -415,13 +421,6 @@ export class DocumentDetailComponent
|
||||
.pipe(
|
||||
switchMap((doc) => {
|
||||
this.documentId = doc.id
|
||||
this.downloadUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId
|
||||
)
|
||||
this.downloadOriginalUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId,
|
||||
true
|
||||
)
|
||||
this.suggestions = null
|
||||
const openDocument = this.openDocumentService.getOpenDocument(
|
||||
this.documentId
|
||||
@@ -810,7 +809,9 @@ export class DocumentDetailComponent
|
||||
this.store.next(newValues)
|
||||
this.openDocumentService.setDirty(this.document, false)
|
||||
this.openDocumentService.save()
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${newValues.title}" saved successfully.`
|
||||
)
|
||||
this.networkActive = false
|
||||
this.error = null
|
||||
if (close) {
|
||||
@@ -824,11 +825,16 @@ export class DocumentDetailComponent
|
||||
error: (error) => {
|
||||
this.networkActive = false
|
||||
if (!this.userCanEdit) {
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${this.document.title}" saved successfully.`
|
||||
)
|
||||
close && this.close()
|
||||
} else {
|
||||
this.error = error.error
|
||||
this.toastService.showError($localize`Error saving document`, error)
|
||||
this.toastService.showError(
|
||||
$localize`Error saving document "${this.document.title}"`,
|
||||
error
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -888,6 +894,10 @@ export class DocumentDetailComponent
|
||||
'view',
|
||||
this.documentListViewService.activeSavedViewId,
|
||||
])
|
||||
} else if (this.componentRouterService.getComponentURLBefore()) {
|
||||
this.router.navigate([
|
||||
this.componentRouterService.getComponentURLBefore(),
|
||||
])
|
||||
} else {
|
||||
this.router.navigate(['documents'])
|
||||
}
|
||||
@@ -953,7 +963,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Reprocess operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.`
|
||||
$localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.`
|
||||
)
|
||||
if (modal) {
|
||||
modal.close()
|
||||
@@ -972,6 +982,52 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
download(original: boolean = false) {
|
||||
this.downloading = true
|
||||
const downloadUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId,
|
||||
original
|
||||
)
|
||||
this.http.get(downloadUrl, { responseType: 'blob' }).subscribe({
|
||||
next: (blob) => {
|
||||
this.downloading = false
|
||||
const blobParts = [blob]
|
||||
const file = new File(
|
||||
blobParts,
|
||||
original
|
||||
? this.document.original_file_name
|
||||
: this.document.archived_file_name,
|
||||
{
|
||||
type: original ? this.document.mime_type : 'application/pdf',
|
||||
}
|
||||
)
|
||||
if (
|
||||
!this.deviceDetectorService.isDesktop() &&
|
||||
navigator.canShare &&
|
||||
navigator.canShare({ files: [file] })
|
||||
) {
|
||||
navigator.share({
|
||||
files: [file],
|
||||
})
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = this.document.title
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.downloading = false
|
||||
this.toastService.showError(
|
||||
$localize`Error downloading document`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return this.documentListViewService.hasNext(this.documentId)
|
||||
}
|
||||
@@ -1017,14 +1073,13 @@ export class DocumentDetailComponent
|
||||
}
|
||||
}
|
||||
|
||||
onZoomSelect(event: Event) {
|
||||
const setting = (event.target as HTMLSelectElement)?.value as ZoomSetting
|
||||
if (ZoomSetting.PageFit === setting) {
|
||||
this.previewZoomSetting = ZoomSetting.One
|
||||
setZoom(setting: ZoomSetting) {
|
||||
if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) {
|
||||
this.previewZoomScale = setting
|
||||
this.previewZoomSetting = ZoomSetting.One
|
||||
} else {
|
||||
this.previewZoomScale = ZoomSetting.PageWidth
|
||||
this.previewZoomSetting = setting
|
||||
this.previewZoomScale = ZoomSetting.PageWidth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1034,6 +1089,14 @@ export class DocumentDetailComponent
|
||||
)
|
||||
}
|
||||
|
||||
isZoomSelected(setting: ZoomSetting): boolean {
|
||||
if (this.previewZoomScale === ZoomSetting.PageFit) {
|
||||
return setting === ZoomSetting.PageFit
|
||||
}
|
||||
|
||||
return this.previewZoomSetting === setting
|
||||
}
|
||||
|
||||
getZoomSettingTitle(setting: ZoomSetting): string {
|
||||
switch (setting) {
|
||||
case ZoomSetting.PageFit:
|
||||
@@ -1267,7 +1330,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Split operation will begin in the background.`
|
||||
$localize`Split operation for "${this.document.title}" will begin in the background.`
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
@@ -1306,7 +1369,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.show({
|
||||
content: $localize`Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
|
||||
content: $localize`Rotation of "${this.document.title}" will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
|
||||
delay: 8000,
|
||||
action: this.close.bind(this),
|
||||
actionName: $localize`Close`,
|
||||
@@ -1346,7 +1409,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.`
|
||||
$localize`Delete pages operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.`
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
|
||||
@@ -1039,6 +1039,7 @@ describe('BulkEditorComponent', () => {
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
expect(documentListViewService.selected.size).toEqual(0)
|
||||
})
|
||||
|
||||
it('should support bulk download with archive, originals or both and file formatting', () => {
|
||||
|
||||
@@ -268,6 +268,9 @@ export class BulkEditorComponent
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: () => {
|
||||
if (args['delete_originals']) {
|
||||
this.list.selected.clear()
|
||||
}
|
||||
this.list.reload()
|
||||
this.list.reduceSelectionToFilter()
|
||||
this.list.selected.forEach((id) => {
|
||||
|
||||
@@ -38,16 +38,16 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
FileStatus,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
|
||||
import { DocumentCardSmallComponent } from './document-card-small/document-card-small.component'
|
||||
import { DocumentListComponent } from './document-list.component'
|
||||
@@ -81,7 +81,7 @@ describe('DocumentListComponent', () => {
|
||||
let fixture: ComponentFixture<DocumentListComponent>
|
||||
let documentListService: DocumentListViewService
|
||||
let documentService: DocumentService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let savedViewService: SavedViewService
|
||||
let router: Router
|
||||
let activatedRoute: ActivatedRoute
|
||||
@@ -112,7 +112,7 @@ describe('DocumentListComponent', () => {
|
||||
|
||||
documentListService = TestBed.inject(DocumentListViewService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
savedViewService = TestBed.inject(SavedViewService)
|
||||
router = TestBed.inject(Router)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
@@ -128,13 +128,24 @@ describe('DocumentListComponent', () => {
|
||||
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
fixture.detectChanges()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reload on document deleted', () => {
|
||||
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
||||
const documentDeletedSubject = new Subject<boolean>()
|
||||
jest
|
||||
.spyOn(websocketStatusService, 'onDocumentDeleted')
|
||||
.mockReturnValue(documentDeletedSubject)
|
||||
fixture.detectChanges()
|
||||
documentDeletedSubject.next(true)
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show score sort fields on fulltext queries', () => {
|
||||
documentListService.filterRules = [
|
||||
{
|
||||
|
||||
@@ -43,7 +43,6 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
|
||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -51,6 +50,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import {
|
||||
filterRulesDiffer,
|
||||
isFullTextFilterRule,
|
||||
@@ -113,7 +113,7 @@ export class DocumentListComponent
|
||||
private router: Router,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public settingsService: SettingsService,
|
||||
private hotKeyService: HotKeyService,
|
||||
@@ -234,13 +234,17 @@ export class DocumentListComponent
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.consumerStatusService
|
||||
this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.list.reload()
|
||||
})
|
||||
|
||||
this.websocketStatusService.onDocumentDeleted().subscribe(() => {
|
||||
this.list.reload()
|
||||
})
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(
|
||||
filter((params) => params.has('id')), // only on saved view e.g. /view/id
|
||||
|
||||
@@ -108,13 +108,16 @@ export class CustomFieldsComponent
|
||||
this.customFieldsService.delete(field).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted field`)
|
||||
this.toastService.showInfo($localize`Deleted field "${field.name}"`)
|
||||
this.customFieldsService.clearCache()
|
||||
this.settingsService.initializeDisplayFields()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting field.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting field "${field.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -214,7 +214,7 @@ describe('MailComponent', () => {
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account "account1"')
|
||||
})
|
||||
|
||||
it('should support process mail account, show error if needed', () => {
|
||||
@@ -231,7 +231,9 @@ describe('MailComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
processSpy.mockReturnValueOnce(of(true))
|
||||
component.processAccount(mailAccounts[0] as MailAccount)
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Processing mail account')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
'Processing mail account "account1"'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support edit / create mail rule, show error if needed', () => {
|
||||
@@ -274,14 +276,14 @@ describe('MailComponent', () => {
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting mail rule'))
|
||||
throwError(() => new Error('error deleting mail rule "rule1"'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule "rule1"')
|
||||
})
|
||||
|
||||
it('should support edit permissions on mail rule objects', () => {
|
||||
|
||||
@@ -200,7 +200,9 @@ export class MailComponent
|
||||
this.mailAccountService.delete(account).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail account`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Deleted mail account "${account.name}"`
|
||||
)
|
||||
this.mailAccountService.clearCache()
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
@@ -210,7 +212,7 @@ export class MailComponent
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail account.`,
|
||||
$localize`Error deleting mail account "${account.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
@@ -221,10 +223,15 @@ export class MailComponent
|
||||
processAccount(account: MailAccount) {
|
||||
this.mailAccountService.processAccount(account).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Processing mail account`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Processing mail account "${account.name}"`
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error processing mail account`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error processing mail account "${account.name}")`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -272,7 +279,10 @@ export class MailComponent
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error toggling rule.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error toggling rule "${rule.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -291,7 +301,9 @@ export class MailComponent
|
||||
this.mailRuleService.delete(rule).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail rule`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Deleted mail rule "${rule.name}"`
|
||||
)
|
||||
this.mailRuleService.clearCache()
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
@@ -300,7 +312,10 @@ export class MailComponent
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting mail rule.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail rule "${rule.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,35 +73,37 @@
|
||||
</td>
|
||||
}
|
||||
<td scope="row">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
@if (object.document_count > 0) {
|
||||
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button>
|
||||
}
|
||||
<div class="btn-toolbar gap-2">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
@if (object.document_count > 0) {
|
||||
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@if (object.document_count > 0) {
|
||||
<div class="btn-group d-none d-sm-inline-block ms-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span>
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (object.document_count > 0) {
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -142,12 +142,17 @@ export class WorkflowsComponent
|
||||
this.workflowService.delete(workflow).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted workflow`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Deleted workflow "${workflow.name}".`
|
||||
)
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting workflow.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting workflow "${workflow.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -158,14 +163,17 @@ export class WorkflowsComponent
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
workflow.enabled
|
||||
? $localize`Enabled workflow`
|
||||
: $localize`Disabled workflow`
|
||||
? $localize`Enabled workflow "${workflow.name}"`
|
||||
: $localize`Disabled workflow "${workflow.name}"`
|
||||
)
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error toggling workflow.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error toggling workflow "${workflow.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ export const SETTINGS_KEYS = {
|
||||
DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted',
|
||||
THEME_COLOR: 'general-settings:theme:color',
|
||||
USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer',
|
||||
PDF_VIEWER_ZOOM_SETTING:
|
||||
'general-settings:document-details:pdf-viewer-zoom-setting',
|
||||
DATE_LOCALE: 'general-settings:date-display:date-locale',
|
||||
DATE_FORMAT: 'general-settings:date-display:date-format',
|
||||
NOTIFICATIONS_CONSUMER_NEW_DOCUMENT:
|
||||
@@ -269,4 +271,9 @@ export const SETTINGS: UiSetting[] = [
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||
type: 'string',
|
||||
default: 'page-width', // ZoomSetting from 'document-detail.component'
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface WebsocketDocumentsDeletedMessage {
|
||||
documents: number[]
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface WebsocketConsumerStatusMessage {
|
||||
export interface WebsocketProgressMessage {
|
||||
filename?: string
|
||||
task_id?: string
|
||||
current_progress?: number
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { ActivationStart, Router } from '@angular/router'
|
||||
import { Subject } from 'rxjs'
|
||||
import { ComponentRouterService } from './component-router.service'
|
||||
|
||||
describe('ComponentRouterService', () => {
|
||||
let service: ComponentRouterService
|
||||
let router: Router
|
||||
let eventsSubject: Subject<any>
|
||||
|
||||
beforeEach(() => {
|
||||
eventsSubject = new Subject<any>()
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ComponentRouterService,
|
||||
{
|
||||
provide: Router,
|
||||
useValue: {
|
||||
events: eventsSubject.asObservable(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
service = TestBed.inject(ComponentRouterService)
|
||||
router = TestBed.inject(Router)
|
||||
})
|
||||
|
||||
it('should add to history and componentHistory on ActivationStart event', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect((service as any).history).toEqual(['test-url'])
|
||||
expect((service as any).componentHistory).toEqual(['TestComponent'])
|
||||
})
|
||||
|
||||
it('should not add duplicate component names to componentHistory', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-1',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-2',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect((service as any).componentHistory.length).toBe(1)
|
||||
expect((service as any).componentHistory).toEqual(['TestComponent'])
|
||||
})
|
||||
|
||||
it('should return the URL of the component before the current one', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-1',
|
||||
component: { name: 'TestComponent1' },
|
||||
} as any)
|
||||
)
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-2',
|
||||
component: { name: 'TestComponent2' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect(service.getComponentURLBefore()).toBe('test-url-1')
|
||||
})
|
||||
|
||||
it('should update the URL of the current component if the same component is loaded via a different URL', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-1',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-2',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect((service as any).history).toEqual(['test-url-2'])
|
||||
})
|
||||
|
||||
it('should return null if there is no previous component', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect(service.getComponentURLBefore()).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivationStart, Event, Router } from '@angular/router'
|
||||
import { filter } from 'rxjs'
|
||||
|
||||
const EXCLUDE_COMPONENTS = ['AppFrameComponent']
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ComponentRouterService {
|
||||
private history: string[] = []
|
||||
private componentHistory: any[] = []
|
||||
|
||||
constructor(private router: Router) {
|
||||
this.router.events
|
||||
.pipe(filter((event: Event) => event instanceof ActivationStart))
|
||||
.subscribe((event: ActivationStart) => {
|
||||
if (
|
||||
this.componentHistory[this.componentHistory.length - 1] !==
|
||||
event.snapshot.component.name &&
|
||||
!EXCLUDE_COMPONENTS.includes(event.snapshot.component.name)
|
||||
) {
|
||||
this.history.push(event.snapshot.url.toString())
|
||||
this.componentHistory.push(event.snapshot.component.name)
|
||||
} else {
|
||||
// Update the URL of the current component in case the same component was loaded via a different URL
|
||||
this.history[this.history.length - 1] = event.snapshot.url.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getComponentURLBefore(): any {
|
||||
if (this.componentHistory.length > 1) {
|
||||
return this.history[this.history.length - 2]
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
import {
|
||||
HttpEventType,
|
||||
HttpResponse,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import WS from 'jest-websocket-mock'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FILE_STATUS_MESSAGES,
|
||||
FileStatusPhase,
|
||||
} from './consumer-status.service'
|
||||
import { DocumentService } from './rest/document.service'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
describe('ConsumerStatusService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let documentService: DocumentService
|
||||
let settingsService: SettingsService
|
||||
|
||||
const server = new WS(
|
||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
||||
{ jsonProtocol: true }
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
ConsumerStatusService,
|
||||
DocumentService,
|
||||
SettingsService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
}
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('should update status on websocket processing progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
expect(status.getProgress()).toEqual(0)
|
||||
|
||||
consumerStatusService.connect()
|
||||
|
||||
consumerStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS)
|
||||
})
|
||||
|
||||
consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
|
||||
})
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 100,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'SUCCESS',
|
||||
message: FILE_STATUS_MESSAGES.finished,
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toEqual(1)
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
})
|
||||
|
||||
it('should update status on websocket failed progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
consumerStatusService.connect()
|
||||
|
||||
consumerStatusService
|
||||
.onDocumentConsumptionFailed()
|
||||
.subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.FAILED)
|
||||
})
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
})
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'FAILED',
|
||||
message: FILE_STATUS_MESSAGES.document_already_exists,
|
||||
})
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should update status on upload progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
|
||||
documentService.uploadDocument({}).subscribe((event) => {
|
||||
if (event.type === HttpEventType.Response) {
|
||||
status.taskId = event.body['task_id']
|
||||
status.message = $localize`Upload complete, waiting...`
|
||||
} else if (event.type === HttpEventType.UploadProgress) {
|
||||
status.updateProgress(
|
||||
FileStatusPhase.UPLOADING,
|
||||
event.loaded,
|
||||
event.total
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
req.event(
|
||||
new HttpResponse({
|
||||
body: {
|
||||
task_id,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
req.event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 100,
|
||||
total: 300,
|
||||
})
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toEqual([status])
|
||||
expect(consumerStatusService.getConsumerStatus()).toEqual([status])
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
req.event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 300,
|
||||
total: 300,
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300
|
||||
})
|
||||
|
||||
it('should support dismiss completed', () => {
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 100,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'SUCCESS',
|
||||
message: 'finished',
|
||||
})
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
consumerStatusService.dismissCompleted()
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
|
||||
consumerStatusService.disconnect()
|
||||
})
|
||||
|
||||
it('should support dismiss', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
|
||||
const status2 = consumerStatusService.newFileUpload('file2.pdf')
|
||||
status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toEqual([status, status2])
|
||||
expect(consumerStatusService.getConsumerStatus()).toEqual([status, status2])
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
status2,
|
||||
])
|
||||
|
||||
consumerStatusService.dismiss(status)
|
||||
expect(consumerStatusService.getConsumerStatus()).toEqual([status2])
|
||||
|
||||
consumerStatusService.dismiss(status2)
|
||||
expect(consumerStatusService.getConsumerStatus()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should support fail', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
|
||||
consumerStatusService.fail(status, 'fail')
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should notify of document created on status message without upload', () => {
|
||||
let detected = false
|
||||
consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
|
||||
detected = true
|
||||
})
|
||||
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 0,
|
||||
max_progress: 100,
|
||||
message: 'new_file',
|
||||
status: 'STARTED',
|
||||
})
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
expect(detected).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should notify of document in progress without upload', () => {
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 12,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
it('should not notify current user if document has different expected owner', () => {
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file1.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 12,
|
||||
owner_id: 1,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
server.send({
|
||||
task_id: '5678',
|
||||
filename: 'file2.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 13,
|
||||
owner_id: 2,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
} from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatusPhase,
|
||||
} from './consumer-status.service'
|
||||
import { UploadDocumentsService } from './upload-documents.service'
|
||||
import {
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
} from './websocket-status.service'
|
||||
|
||||
const files = [
|
||||
{
|
||||
@@ -45,14 +45,14 @@ const fileList = {
|
||||
describe('UploadDocumentsService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
UploadDocumentsService,
|
||||
ConsumerStatusService,
|
||||
WebsocketStatusService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
@@ -60,7 +60,7 @@ describe('UploadDocumentsService', () => {
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -80,11 +80,11 @@ describe('UploadDocumentsService', () => {
|
||||
it('updates progress during upload and failure', () => {
|
||||
uploadDocumentsService.uploadFiles(fileList)
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
2
|
||||
)
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toHaveLength(0)
|
||||
|
||||
const req = httpTestingController.match(
|
||||
@@ -98,7 +98,7 @@ describe('UploadDocumentsService', () => {
|
||||
})
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('UploadDocumentsService', () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(0)
|
||||
|
||||
req[0].flush(
|
||||
@@ -122,7 +122,7 @@ describe('UploadDocumentsService', () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(1)
|
||||
|
||||
uploadDocumentsService.uploadFiles(fileList)
|
||||
@@ -140,7 +140,7 @@ describe('UploadDocumentsService', () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
).toHaveLength(2)
|
||||
})
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import { HttpEventType } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { Subscription } from 'rxjs'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatusPhase,
|
||||
} from './consumer-status.service'
|
||||
import { DocumentService } from './rest/document.service'
|
||||
import {
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
} from './websocket-status.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -16,7 +16,7 @@ export class UploadDocumentsService {
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private consumerStatusService: ConsumerStatusService
|
||||
private websocketStatusService: WebsocketStatusService
|
||||
) {}
|
||||
|
||||
onNgxFileDrop(files: NgxFileDropEntry[]) {
|
||||
@@ -37,7 +37,7 @@ export class UploadDocumentsService {
|
||||
private uploadFile(file: File) {
|
||||
let formData = new FormData()
|
||||
formData.append('document', file, file.name)
|
||||
let status = this.consumerStatusService.newFileUpload(file.name)
|
||||
let status = this.websocketStatusService.newFileUpload(file.name)
|
||||
|
||||
status.message = $localize`Connecting...`
|
||||
|
||||
@@ -61,11 +61,11 @@ export class UploadDocumentsService {
|
||||
error: (error) => {
|
||||
switch (error.status) {
|
||||
case 400: {
|
||||
this.consumerStatusService.fail(status, error.error.document)
|
||||
this.websocketStatusService.fail(status, error.error.document)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
this.consumerStatusService.fail(
|
||||
this.websocketStatusService.fail(
|
||||
status,
|
||||
$localize`HTTP error: ${error.status} ${error.statusText}`
|
||||
)
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import {
|
||||
HttpEventType,
|
||||
HttpResponse,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import WS from 'jest-websocket-mock'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { DocumentService } from './rest/document.service'
|
||||
import { SettingsService } from './settings.service'
|
||||
import {
|
||||
FILE_STATUS_MESSAGES,
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
WebsocketStatusType,
|
||||
} from './websocket-status.service'
|
||||
|
||||
describe('ConsumerStatusService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let documentService: DocumentService
|
||||
let settingsService: SettingsService
|
||||
|
||||
const server = new WS(
|
||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
||||
{ jsonProtocol: true }
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
WebsocketStatusService,
|
||||
DocumentService,
|
||||
SettingsService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
}
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('should update status on websocket processing progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = websocketStatusService.newFileUpload('file.pdf')
|
||||
expect(status.getProgress()).toEqual(0)
|
||||
|
||||
websocketStatusService.connect()
|
||||
|
||||
websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS)
|
||||
})
|
||||
|
||||
websocketStatusService.onDocumentDetected().subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
|
||||
})
|
||||
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'WORKING',
|
||||
},
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 100,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'SUCCESS',
|
||||
message: FILE_STATUS_MESSAGES.finished,
|
||||
},
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toEqual(1)
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
|
||||
websocketStatusService.disconnect()
|
||||
})
|
||||
|
||||
it('should update status on websocket failed progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = websocketStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
websocketStatusService.connect()
|
||||
|
||||
websocketStatusService
|
||||
.onDocumentConsumptionFailed()
|
||||
.subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.FAILED)
|
||||
})
|
||||
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
},
|
||||
})
|
||||
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'FAILED',
|
||||
message: FILE_STATUS_MESSAGES.document_already_exists,
|
||||
},
|
||||
})
|
||||
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should update status on upload progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = websocketStatusService.newFileUpload('file.pdf')
|
||||
|
||||
documentService.uploadDocument({}).subscribe((event) => {
|
||||
if (event.type === HttpEventType.Response) {
|
||||
status.taskId = event.body['task_id']
|
||||
status.message = $localize`Upload complete, waiting...`
|
||||
} else if (event.type === HttpEventType.UploadProgress) {
|
||||
status.updateProgress(
|
||||
FileStatusPhase.UPLOADING,
|
||||
event.loaded,
|
||||
event.total
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
req.event(
|
||||
new HttpResponse({
|
||||
body: {
|
||||
task_id,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
req.event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 100,
|
||||
total: 300,
|
||||
})
|
||||
|
||||
expect(
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toEqual([status])
|
||||
expect(websocketStatusService.getConsumerStatus()).toEqual([status])
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
req.event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 300,
|
||||
total: 300,
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300
|
||||
})
|
||||
|
||||
it('should support dismiss completed', () => {
|
||||
websocketStatusService.connect()
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 100,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'SUCCESS',
|
||||
message: 'finished',
|
||||
},
|
||||
})
|
||||
|
||||
expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
websocketStatusService.dismissCompleted()
|
||||
expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(0)
|
||||
websocketStatusService.disconnect()
|
||||
})
|
||||
|
||||
it('should support dismiss', () => {
|
||||
const task_id = '1234'
|
||||
const status = websocketStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
|
||||
const status2 = websocketStatusService.newFileUpload('file2.pdf')
|
||||
status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
|
||||
expect(
|
||||
websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toEqual([status, status2])
|
||||
expect(websocketStatusService.getConsumerStatus()).toEqual([
|
||||
status,
|
||||
status2,
|
||||
])
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
status2,
|
||||
])
|
||||
|
||||
websocketStatusService.dismiss(status)
|
||||
expect(websocketStatusService.getConsumerStatus()).toEqual([status2])
|
||||
|
||||
websocketStatusService.dismiss(status2)
|
||||
expect(websocketStatusService.getConsumerStatus()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should support fail', () => {
|
||||
const task_id = '1234'
|
||||
const status = websocketStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(0)
|
||||
websocketStatusService.fail(status, 'fail')
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should notify of document created on status message without upload', () => {
|
||||
let detected = false
|
||||
websocketStatusService.onDocumentDetected().subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
|
||||
detected = true
|
||||
})
|
||||
|
||||
websocketStatusService.connect()
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 0,
|
||||
max_progress: 100,
|
||||
message: 'new_file',
|
||||
status: 'STARTED',
|
||||
},
|
||||
})
|
||||
|
||||
websocketStatusService.disconnect()
|
||||
expect(detected).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should notify of document in progress without upload', () => {
|
||||
websocketStatusService.connect()
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 12,
|
||||
status: 'WORKING',
|
||||
},
|
||||
})
|
||||
|
||||
websocketStatusService.disconnect()
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
it('should not notify current user if document has different expected owner', () => {
|
||||
websocketStatusService.connect()
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id: '1234',
|
||||
filename: 'file1.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 12,
|
||||
owner_id: 1,
|
||||
status: 'WORKING',
|
||||
},
|
||||
})
|
||||
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
data: {
|
||||
task_id: '5678',
|
||||
filename: 'file2.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 13,
|
||||
owner_id: 2,
|
||||
status: 'WORKING',
|
||||
},
|
||||
})
|
||||
|
||||
websocketStatusService.disconnect()
|
||||
expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
it('should trigger deleted subject on document deleted', () => {
|
||||
let deleted = false
|
||||
websocketStatusService.onDocumentDeleted().subscribe(() => {
|
||||
deleted = true
|
||||
})
|
||||
|
||||
websocketStatusService.connect()
|
||||
server.send({
|
||||
type: WebsocketStatusType.DOCUMENTS_DELETED,
|
||||
data: {
|
||||
documents: [1, 2, 3],
|
||||
},
|
||||
})
|
||||
|
||||
websocketStatusService.disconnect()
|
||||
expect(deleted).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+74
-51
@@ -1,9 +1,15 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Subject } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { WebsocketConsumerStatusMessage } from '../data/websocket-consumer-status-message'
|
||||
import { WebsocketDocumentsDeletedMessage } from '../data/websocket-documents-deleted-message'
|
||||
import { WebsocketProgressMessage } from '../data/websocket-progress-message'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
export enum WebsocketStatusType {
|
||||
STATUS_UPDATE = 'status_update',
|
||||
DOCUMENTS_DELETED = 'documents_deleted',
|
||||
}
|
||||
|
||||
// see ProgressStatusOptions in src/documents/plugins/helpers.py
|
||||
export enum FileStatusPhase {
|
||||
STARTED = 0,
|
||||
@@ -85,7 +91,7 @@ export class FileStatus {
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConsumerStatusService {
|
||||
export class WebsocketStatusService {
|
||||
constructor(private settingsService: SettingsService) {}
|
||||
|
||||
private statusWebSocket: WebSocket
|
||||
@@ -95,6 +101,7 @@ export class ConsumerStatusService {
|
||||
private documentDetectedSubject = new Subject<FileStatus>()
|
||||
private documentConsumptionFinishedSubject = new Subject<FileStatus>()
|
||||
private documentConsumptionFailedSubject = new Subject<FileStatus>()
|
||||
private documentDeletedSubject = new Subject<boolean>()
|
||||
|
||||
private get(taskId: string, filename?: string) {
|
||||
let status =
|
||||
@@ -145,63 +152,75 @@ export class ConsumerStatusService {
|
||||
this.statusWebSocket = new WebSocket(
|
||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`
|
||||
)
|
||||
this.statusWebSocket.onmessage = (ev) => {
|
||||
let statusMessage: WebsocketConsumerStatusMessage = JSON.parse(ev['data'])
|
||||
this.statusWebSocket.onmessage = (ev: MessageEvent) => {
|
||||
const {
|
||||
type,
|
||||
data: messageData,
|
||||
}: {
|
||||
type: WebsocketStatusType
|
||||
data: WebsocketProgressMessage | WebsocketDocumentsDeletedMessage
|
||||
} = JSON.parse(ev.data)
|
||||
|
||||
// fallback if backend didn't restrict message
|
||||
if (
|
||||
statusMessage.owner_id &&
|
||||
statusMessage.owner_id !== this.settingsService.currentUser?.id &&
|
||||
!this.settingsService.currentUser?.is_superuser
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let statusMessageGet = this.get(
|
||||
statusMessage.task_id,
|
||||
statusMessage.filename
|
||||
)
|
||||
let status = statusMessageGet.status
|
||||
let created = statusMessageGet.created
|
||||
|
||||
status.updateProgress(
|
||||
FileStatusPhase.WORKING,
|
||||
statusMessage.current_progress,
|
||||
statusMessage.max_progress
|
||||
)
|
||||
if (
|
||||
statusMessage.message &&
|
||||
statusMessage.message in FILE_STATUS_MESSAGES
|
||||
) {
|
||||
status.message = FILE_STATUS_MESSAGES[statusMessage.message]
|
||||
} else if (statusMessage.message) {
|
||||
status.message = statusMessage.message
|
||||
}
|
||||
status.documentId = statusMessage.document_id
|
||||
|
||||
if (statusMessage.status in FileStatusPhase) {
|
||||
status.phase = FileStatusPhase[statusMessage.status]
|
||||
}
|
||||
|
||||
switch (status.phase) {
|
||||
case FileStatusPhase.STARTED:
|
||||
if (created) this.documentDetectedSubject.next(status)
|
||||
switch (type) {
|
||||
case WebsocketStatusType.DOCUMENTS_DELETED:
|
||||
this.documentDeletedSubject.next(true)
|
||||
break
|
||||
|
||||
case FileStatusPhase.SUCCESS:
|
||||
this.documentConsumptionFinishedSubject.next(status)
|
||||
break
|
||||
|
||||
case FileStatusPhase.FAILED:
|
||||
this.documentConsumptionFailedSubject.next(status)
|
||||
break
|
||||
|
||||
default:
|
||||
case WebsocketStatusType.STATUS_UPDATE:
|
||||
this.handleProgressUpdate(messageData as WebsocketProgressMessage)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleProgressUpdate(messageData: WebsocketProgressMessage) {
|
||||
// fallback if backend didn't restrict message
|
||||
if (
|
||||
messageData.owner_id &&
|
||||
messageData.owner_id !== this.settingsService.currentUser?.id &&
|
||||
!this.settingsService.currentUser?.is_superuser
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let statusMessageGet = this.get(messageData.task_id, messageData.filename)
|
||||
let status = statusMessageGet.status
|
||||
let created = statusMessageGet.created
|
||||
|
||||
status.updateProgress(
|
||||
FileStatusPhase.WORKING,
|
||||
messageData.current_progress,
|
||||
messageData.max_progress
|
||||
)
|
||||
if (messageData.message && messageData.message in FILE_STATUS_MESSAGES) {
|
||||
status.message = FILE_STATUS_MESSAGES[messageData.message]
|
||||
} else if (messageData.message) {
|
||||
status.message = messageData.message
|
||||
}
|
||||
status.documentId = messageData.document_id
|
||||
|
||||
if (messageData.status in FileStatusPhase) {
|
||||
status.phase = FileStatusPhase[messageData.status]
|
||||
}
|
||||
|
||||
switch (status.phase) {
|
||||
case FileStatusPhase.STARTED:
|
||||
if (created) this.documentDetectedSubject.next(status)
|
||||
break
|
||||
|
||||
case FileStatusPhase.SUCCESS:
|
||||
this.documentConsumptionFinishedSubject.next(status)
|
||||
break
|
||||
|
||||
case FileStatusPhase.FAILED:
|
||||
this.documentConsumptionFailedSubject.next(status)
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fail(status: FileStatus, message: string) {
|
||||
status.message = message
|
||||
status.phase = FileStatusPhase.FAILED
|
||||
@@ -250,4 +269,8 @@ export class ConsumerStatusService {
|
||||
onDocumentDetected() {
|
||||
return this.documentDetectedSubject
|
||||
}
|
||||
|
||||
onDocumentDeleted() {
|
||||
return this.documentDeletedSubject
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
from documents.permissions import set_permissions_for_object
|
||||
from documents.plugins.helpers import DocumentsStatusManager
|
||||
from documents.tasks import bulk_update_documents
|
||||
from documents.tasks import consume_file
|
||||
from documents.tasks import update_document_content_maybe_archive_file
|
||||
@@ -219,6 +220,9 @@ def delete(doc_ids: list[int]) -> Literal["OK"]:
|
||||
with index.open_index_writer() as writer:
|
||||
for id in doc_ids:
|
||||
index.remove_document_by_id(writer, id)
|
||||
|
||||
status_mgr = DocumentsStatusManager()
|
||||
status_mgr.send_documents_deleted(doc_ids)
|
||||
except Exception as e:
|
||||
if "Data too long for column" in str(e):
|
||||
logger.warning(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import pickle
|
||||
import re
|
||||
import time
|
||||
import warnings
|
||||
from collections.abc import Iterator
|
||||
from hashlib import sha256
|
||||
@@ -141,6 +142,19 @@ class DocumentClassifier:
|
||||
):
|
||||
raise IncompatibleClassifierVersionError("sklearn version update")
|
||||
|
||||
def set_last_checked(self) -> None:
|
||||
# save a timestamp of the last time we checked for retraining to a file
|
||||
with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("w") as f:
|
||||
f.write(str(time.time()))
|
||||
|
||||
def get_last_checked(self) -> float | None:
|
||||
# load the timestamp of the last time we checked for retraining
|
||||
try:
|
||||
with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("r") as f:
|
||||
return float(f.read())
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
return None
|
||||
|
||||
def save(self) -> None:
|
||||
target_file: Path = settings.MODEL_FILE
|
||||
target_file_temp: Path = target_file.with_suffix(".pickle.part")
|
||||
@@ -161,6 +175,7 @@ class DocumentClassifier:
|
||||
pickle.dump(self.storage_path_classifier, f)
|
||||
|
||||
target_file_temp.rename(target_file)
|
||||
self.set_last_checked()
|
||||
|
||||
def train(self) -> bool:
|
||||
# Get non-inbox documents
|
||||
@@ -229,6 +244,7 @@ class DocumentClassifier:
|
||||
and self.last_doc_change_time >= latest_doc_change
|
||||
) and self.last_auto_type_hash == hasher.digest():
|
||||
logger.info("No updates since last training")
|
||||
self.set_last_checked()
|
||||
# Set the classifier information into the cache
|
||||
# Caching for 50 minutes, so slightly less than the normal retrain time
|
||||
cache.set(
|
||||
|
||||
+22
-14
@@ -4,6 +4,7 @@ import os
|
||||
import tempfile
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import magic
|
||||
from django.conf import settings
|
||||
@@ -154,7 +155,11 @@ class ConsumerPlugin(
|
||||
"""
|
||||
Confirm the input file still exists where it should
|
||||
"""
|
||||
if not os.path.isfile(self.input_doc.original_file):
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self.input_doc.original_file, Path), (
|
||||
self.input_doc.original_file
|
||||
)
|
||||
if not self.input_doc.original_file.is_file():
|
||||
self._fail(
|
||||
ConsumerStatusShortMessage.FILE_NOT_FOUND,
|
||||
f"Cannot consume {self.input_doc.original_file}: File not found.",
|
||||
@@ -164,7 +169,7 @@ class ConsumerPlugin(
|
||||
"""
|
||||
Using the MD5 of the file, check this exact file doesn't already exist
|
||||
"""
|
||||
with open(self.input_doc.original_file, "rb") as f:
|
||||
with Path(self.input_doc.original_file).open("rb") as f:
|
||||
checksum = hashlib.md5(f.read()).hexdigest()
|
||||
existing_doc = Document.global_objects.filter(
|
||||
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
||||
@@ -178,7 +183,7 @@ class ConsumerPlugin(
|
||||
log_msg += " Note: existing document is in the trash."
|
||||
|
||||
if settings.CONSUMER_DELETE_DUPLICATES:
|
||||
os.unlink(self.input_doc.original_file)
|
||||
Path(self.input_doc.original_file).unlink()
|
||||
self._fail(
|
||||
msg,
|
||||
log_msg,
|
||||
@@ -237,7 +242,7 @@ class ConsumerPlugin(
|
||||
if not settings.PRE_CONSUME_SCRIPT:
|
||||
return
|
||||
|
||||
if not os.path.isfile(settings.PRE_CONSUME_SCRIPT):
|
||||
if not Path(settings.PRE_CONSUME_SCRIPT).is_file():
|
||||
self._fail(
|
||||
ConsumerStatusShortMessage.PRE_CONSUME_SCRIPT_NOT_FOUND,
|
||||
f"Configured pre-consume script "
|
||||
@@ -280,7 +285,7 @@ class ConsumerPlugin(
|
||||
if not settings.POST_CONSUME_SCRIPT:
|
||||
return
|
||||
|
||||
if not os.path.isfile(settings.POST_CONSUME_SCRIPT):
|
||||
if not Path(settings.POST_CONSUME_SCRIPT).is_file():
|
||||
self._fail(
|
||||
ConsumerStatusShortMessage.POST_CONSUME_SCRIPT_NOT_FOUND,
|
||||
f"Configured post-consume script "
|
||||
@@ -582,7 +587,7 @@ class ConsumerPlugin(
|
||||
document.thumbnail_path,
|
||||
)
|
||||
|
||||
if archive_path and os.path.isfile(archive_path):
|
||||
if archive_path and Path(archive_path).is_file():
|
||||
document.archive_filename = generate_unique_filename(
|
||||
document,
|
||||
archive_filename=True,
|
||||
@@ -594,7 +599,7 @@ class ConsumerPlugin(
|
||||
document.archive_path,
|
||||
)
|
||||
|
||||
with open(archive_path, "rb") as f:
|
||||
with Path(archive_path).open("rb") as f:
|
||||
document.archive_checksum = hashlib.md5(
|
||||
f.read(),
|
||||
).hexdigest()
|
||||
@@ -612,14 +617,14 @@ class ConsumerPlugin(
|
||||
self.unmodified_original.unlink()
|
||||
|
||||
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
|
||||
shadow_file = os.path.join(
|
||||
os.path.dirname(self.input_doc.original_file),
|
||||
"._" + os.path.basename(self.input_doc.original_file),
|
||||
shadow_file = (
|
||||
Path(self.input_doc.original_file).parent
|
||||
/ f"._{Path(self.input_doc.original_file).name}"
|
||||
)
|
||||
|
||||
if os.path.isfile(shadow_file):
|
||||
if Path(shadow_file).is_file():
|
||||
self.log.debug(f"Deleting file {shadow_file}")
|
||||
os.unlink(shadow_file)
|
||||
Path(shadow_file).unlink()
|
||||
|
||||
except Exception as e:
|
||||
self._fail(
|
||||
@@ -704,7 +709,7 @@ class ConsumerPlugin(
|
||||
create_date = date
|
||||
self.log.debug(f"Creation date from parse_date: {create_date}")
|
||||
else:
|
||||
stats = os.stat(self.input_doc.original_file)
|
||||
stats = Path(self.input_doc.original_file).stat()
|
||||
create_date = timezone.make_aware(
|
||||
datetime.datetime.fromtimestamp(stats.st_mtime),
|
||||
)
|
||||
@@ -800,7 +805,10 @@ class ConsumerPlugin(
|
||||
) # adds to document
|
||||
|
||||
def _write(self, storage_type, source, target):
|
||||
with open(source, "rb") as read_file, open(target, "wb") as write_file:
|
||||
with (
|
||||
Path(source).open("rb") as read_file,
|
||||
Path(target).open("wb") as write_file,
|
||||
):
|
||||
write_file.write(read_file.read())
|
||||
|
||||
# Attempt to copy file's original stats, but it's ok if we can't
|
||||
|
||||
@@ -41,7 +41,7 @@ DATE_REGEX = re.compile(
|
||||
r"(\b|(?!=([_-])))(\d{1,2}[\. ]+[a-zéûäëčžúřěáíóńźçŞğü]{3,9} \d{4}|[a-zéûäëčžúřěáíóńźçŞğü]{3,9} \d{1,2}, \d{4})(\b|(?=([_-])))|"
|
||||
r"(\b|(?!=([_-])))([^\W\d_]{3,9} \d{1,2}, (\d{4}))(\b|(?=([_-])))|"
|
||||
r"(\b|(?!=([_-])))([^\W\d_]{3,9} \d{4})(\b|(?=([_-])))|"
|
||||
r"(\b|(?!=([_-])))(\d{1,2}[^ ]{2}[\. ]+[^ ]{3,9}[ \.\/-]\d{4})(\b|(?=([_-])))|"
|
||||
r"(\b|(?!=([_-])))(\d{1,2}[^ 0-9]{2}[\. ]+[^ ]{3,9}[ \.\/-]\d{4})(\b|(?=([_-])))|"
|
||||
r"(\b|(?!=([_-])))(\b\d{1,2}[ \.\/-][a-zéûäëčžúřěáíóńźçŞğü]{3}[ \.\/-]\d{4})(\b|(?=([_-])))",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -15,16 +15,14 @@ class ProgressStatusOptions(str, enum.Enum):
|
||||
FAILED = "FAILED"
|
||||
|
||||
|
||||
class ProgressManager:
|
||||
class BaseStatusManager:
|
||||
"""
|
||||
Handles sending of progress information via the channel layer, with proper management
|
||||
of the open/close of the layer to ensure messages go out and everything is cleaned up
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str, task_id: str | None = None) -> None:
|
||||
self.filename = filename
|
||||
def __init__(self) -> None:
|
||||
self._channel: RedisPubSubChannelLayer | None = None
|
||||
self.task_id = task_id
|
||||
|
||||
def __enter__(self):
|
||||
self.open()
|
||||
@@ -49,6 +47,24 @@ class ProgressManager:
|
||||
async_to_sync(self._channel.flush)
|
||||
self._channel = None
|
||||
|
||||
def send(self, payload: dict[str, str | int | None]) -> None:
|
||||
# Ensure the layer is open
|
||||
self.open()
|
||||
|
||||
# Just for IDEs
|
||||
if TYPE_CHECKING:
|
||||
assert self._channel is not None
|
||||
|
||||
# Construct and send the update
|
||||
async_to_sync(self._channel.group_send)("status_updates", payload)
|
||||
|
||||
|
||||
class ProgressManager(BaseStatusManager):
|
||||
def __init__(self, filename: str | None = None, task_id: str | None = None) -> None:
|
||||
super().__init__()
|
||||
self.filename = filename
|
||||
self.task_id = task_id
|
||||
|
||||
def send_progress(
|
||||
self,
|
||||
status: ProgressStatusOptions,
|
||||
@@ -57,13 +73,6 @@ class ProgressManager:
|
||||
max_progress: int,
|
||||
extra_args: dict[str, str | int | None] | None = None,
|
||||
) -> None:
|
||||
# Ensure the layer is open
|
||||
self.open()
|
||||
|
||||
# Just for IDEs
|
||||
if TYPE_CHECKING:
|
||||
assert self._channel is not None
|
||||
|
||||
payload = {
|
||||
"type": "status_update",
|
||||
"data": {
|
||||
@@ -78,5 +87,16 @@ class ProgressManager:
|
||||
if extra_args is not None:
|
||||
payload["data"].update(extra_args)
|
||||
|
||||
# Construct and send the update
|
||||
async_to_sync(self._channel.group_send)("status_updates", payload)
|
||||
self.send(payload)
|
||||
|
||||
|
||||
class DocumentsStatusManager(BaseStatusManager):
|
||||
def send_documents_deleted(self, documents: list[int]) -> None:
|
||||
payload = {
|
||||
"type": "documents_deleted",
|
||||
"data": {
|
||||
"documents": documents,
|
||||
},
|
||||
}
|
||||
|
||||
self.send(payload)
|
||||
|
||||
@@ -353,7 +353,7 @@ def cleanup_document_deletion(sender, instance, **kwargs):
|
||||
f"{filename} could not be deleted: {e}",
|
||||
)
|
||||
elif filename and not os.path.isfile(filename):
|
||||
logger.warn(f"Expected {filename} tp exist, but it did not")
|
||||
logger.warning(f"Expected {filename} to exist, but it did not")
|
||||
|
||||
delete_empty_directories(
|
||||
os.path.dirname(instance.source_path),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
@@ -15,9 +14,10 @@ from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import SampleDirMixin
|
||||
|
||||
|
||||
class TestBulkDownload(DirectoriesMixin, APITestCase):
|
||||
class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
|
||||
ENDPOINT = "/api/documents/bulk_download/"
|
||||
|
||||
def setUp(self):
|
||||
@@ -51,22 +51,10 @@ class TestBulkDownload(DirectoriesMixin, APITestCase):
|
||||
archive_checksum="D",
|
||||
)
|
||||
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
self.doc2.source_path,
|
||||
)
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.png"),
|
||||
self.doc2b.source_path,
|
||||
)
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"),
|
||||
self.doc3.source_path,
|
||||
)
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"),
|
||||
self.doc3.archive_path,
|
||||
)
|
||||
shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc2.source_path)
|
||||
shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path)
|
||||
shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path)
|
||||
shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path)
|
||||
|
||||
def test_download_originals(self):
|
||||
response = self.client.post(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
@@ -8,6 +7,7 @@ from binascii import hexlify
|
||||
from datetime import date
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
import celery
|
||||
@@ -171,19 +171,18 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
content = b"This is a test"
|
||||
content_thumbnail = b"thumbnail content"
|
||||
|
||||
with open(filename, "wb") as f:
|
||||
with Path(filename).open("wb") as f:
|
||||
f.write(content)
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="none",
|
||||
filename=os.path.basename(filename),
|
||||
filename=Path(filename).name,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"),
|
||||
"wb",
|
||||
) as f:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self.dirs.thumbnail_dir, Path), self.dirs.thumbnail_dir
|
||||
with (self.dirs.thumbnail_dir / f"{doc.pk:07d}.webp").open("wb") as f:
|
||||
f.write(content_thumbnail)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/download/")
|
||||
@@ -217,7 +216,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
content = b"This is a test"
|
||||
content_thumbnail = b"thumbnail content"
|
||||
|
||||
with open(filename, "wb") as f:
|
||||
with Path(filename).open("wb") as f:
|
||||
f.write(content)
|
||||
|
||||
user1 = User.objects.create_user(username="test1")
|
||||
@@ -229,15 +228,12 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="none",
|
||||
filename=os.path.basename(filename),
|
||||
filename=Path(filename).name,
|
||||
mime_type="application/pdf",
|
||||
owner=user1,
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"),
|
||||
"wb",
|
||||
) as f:
|
||||
with (Path(self.dirs.thumbnail_dir) / f"{doc.pk:07d}.webp").open("wb") as f:
|
||||
f.write(content_thumbnail)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/download/")
|
||||
@@ -272,10 +268,10 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
with open(doc.source_path, "wb") as f:
|
||||
with Path(doc.source_path).open("wb") as f:
|
||||
f.write(content)
|
||||
|
||||
with open(doc.archive_path, "wb") as f:
|
||||
with Path(doc.archive_path).open("wb") as f:
|
||||
f.write(content_archive)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/download/")
|
||||
@@ -305,7 +301,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
def test_document_actions_not_existing_file(self):
|
||||
doc = Document.objects.create(
|
||||
title="none",
|
||||
filename=os.path.basename("asd"),
|
||||
filename=Path("asd").name,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
@@ -1026,10 +1022,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f},
|
||||
@@ -1061,10 +1054,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{
|
||||
@@ -1095,10 +1085,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"documenst": f},
|
||||
@@ -1111,10 +1098,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.zip"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.zip").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f},
|
||||
@@ -1127,10 +1111,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "title": "my custom title"},
|
||||
@@ -1152,10 +1133,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
)
|
||||
|
||||
c = Correspondent.objects.create(name="test-corres")
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "correspondent": c.id},
|
||||
@@ -1176,10 +1154,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "correspondent": 3456},
|
||||
@@ -1194,10 +1169,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
)
|
||||
|
||||
dt = DocumentType.objects.create(name="invoice")
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "document_type": dt.id},
|
||||
@@ -1218,10 +1190,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "document_type": 34578},
|
||||
@@ -1236,10 +1205,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
)
|
||||
|
||||
sp = StoragePath.objects.create(name="invoices")
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "storage_path": sp.id},
|
||||
@@ -1260,10 +1226,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "storage_path": 34578},
|
||||
@@ -1279,10 +1242,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
t1 = Tag.objects.create(name="tag1")
|
||||
t2 = Tag.objects.create(name="tag2")
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "tags": [t2.id, t1.id]},
|
||||
@@ -1305,10 +1265,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
t1 = Tag.objects.create(name="tag1")
|
||||
t2 = Tag.objects.create(name="tag2")
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "tags": [t2.id, t1.id, 734563]},
|
||||
@@ -1332,10 +1289,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
0,
|
||||
tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
|
||||
)
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "created": created},
|
||||
@@ -1353,10 +1307,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "archive_serial_number": 500},
|
||||
@@ -1385,10 +1336,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
data_type=CustomField.FieldDataType.STRING,
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{
|
||||
@@ -1417,10 +1365,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "invalid_pdf.pdf"),
|
||||
"rb",
|
||||
) as f:
|
||||
with (Path(__file__).parent / "samples" / "invalid_pdf.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f},
|
||||
@@ -1437,14 +1382,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
archive_filename="archive.pdf",
|
||||
)
|
||||
|
||||
source_file = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"samples",
|
||||
"documents",
|
||||
"thumbnails",
|
||||
"0000001.webp",
|
||||
source_file: Path = (
|
||||
Path(__file__).parent
|
||||
/ "samples"
|
||||
/ "documents"
|
||||
/ "thumbnails"
|
||||
/ "0000001.webp"
|
||||
)
|
||||
archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
|
||||
archive_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
|
||||
|
||||
shutil.copy(source_file, doc.source_path)
|
||||
shutil.copy(archive_file, doc.archive_path)
|
||||
@@ -1460,8 +1405,8 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
self.assertGreater(len(meta["archive_metadata"]), 0)
|
||||
self.assertEqual(meta["media_filename"], "file.pdf")
|
||||
self.assertEqual(meta["archive_media_filename"], "archive.pdf")
|
||||
self.assertEqual(meta["original_size"], os.stat(source_file).st_size)
|
||||
self.assertEqual(meta["archive_size"], os.stat(archive_file).st_size)
|
||||
self.assertEqual(meta["original_size"], Path(source_file).stat().st_size)
|
||||
self.assertEqual(meta["archive_size"], Path(archive_file).stat().st_size)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -1477,10 +1422,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"),
|
||||
doc.source_path,
|
||||
)
|
||||
shutil.copy(Path(__file__).parent / "samples" / "simple.pdf", doc.source_path)
|
||||
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -1939,9 +1881,9 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
def test_get_logs(self):
|
||||
log_data = "test\ntest2\n"
|
||||
with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f:
|
||||
with (Path(settings.LOGGING_DIR) / "mail.log").open("w") as f:
|
||||
f.write(log_data)
|
||||
with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
|
||||
with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
|
||||
f.write(log_data)
|
||||
response = self.client.get("/api/logs/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -1949,7 +1891,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
def test_get_logs_only_when_exist(self):
|
||||
log_data = "test\ntest2\n"
|
||||
with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
|
||||
with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
|
||||
f.write(log_data)
|
||||
response = self.client.get("/api/logs/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -1966,7 +1908,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
def test_get_log(self):
|
||||
log_data = "test\ntest2\n"
|
||||
with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f:
|
||||
with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
|
||||
f.write(log_data)
|
||||
response = self.client.get("/api/logs/paperless/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
+6
-11
@@ -15,7 +15,6 @@ from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pathvalidate
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
@@ -2174,18 +2173,14 @@ class SystemStatusView(PassUserMixin):
|
||||
classifier_status = "WARNING"
|
||||
raise FileNotFoundError(classifier_error)
|
||||
classifier_status = "OK"
|
||||
task_result_model = apps.get_model("django_celery_results", "taskresult")
|
||||
result = (
|
||||
task_result_model.objects.filter(
|
||||
task_name="documents.tasks.train_classifier",
|
||||
status="SUCCESS",
|
||||
classifier_last_trained = (
|
||||
make_aware(
|
||||
datetime.fromtimestamp(classifier.get_last_checked()),
|
||||
)
|
||||
.order_by(
|
||||
"-date_done",
|
||||
)
|
||||
.first()
|
||||
if settings.MODEL_FILE.exists()
|
||||
and classifier.get_last_checked() is not None
|
||||
else None
|
||||
)
|
||||
classifier_last_trained = result.date_done if result else None
|
||||
except Exception as e:
|
||||
if classifier_status is None:
|
||||
classifier_status = "ERROR"
|
||||
|
||||
@@ -41,4 +41,10 @@ class StatusConsumer(WebsocketConsumer):
|
||||
self.close()
|
||||
else:
|
||||
if self._is_owner_or_unowned(event["data"]):
|
||||
self.send(json.dumps(event["data"]))
|
||||
self.send(json.dumps(event))
|
||||
|
||||
def documents_deleted(self, event):
|
||||
if not self._authenticated():
|
||||
self.close()
|
||||
else:
|
||||
self.send(json.dumps(event))
|
||||
|
||||
@@ -162,7 +162,7 @@ class SocialAccountSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
email = serializers.EmailField(allow_null=False)
|
||||
email = serializers.EmailField(allow_blank=True, required=False)
|
||||
password = ObfuscatedUserPasswordField(required=False, allow_null=False)
|
||||
auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key")
|
||||
social_accounts = SocialAccountSerializer(
|
||||
|
||||
@@ -5,6 +5,9 @@ from channels.testing import WebsocketCommunicator
|
||||
from django.test import TestCase
|
||||
from django.test import override_settings
|
||||
|
||||
from documents.plugins.helpers import DocumentsStatusManager
|
||||
from documents.plugins.helpers import ProgressManager
|
||||
from documents.plugins.helpers import ProgressStatusOptions
|
||||
from paperless.asgi import application
|
||||
|
||||
TEST_CHANNEL_LAYERS = {
|
||||
@@ -22,6 +25,39 @@ class TestWebSockets(TestCase):
|
||||
self.assertFalse(connected)
|
||||
await communicator.disconnect()
|
||||
|
||||
@mock.patch("paperless.consumers.StatusConsumer.close")
|
||||
@mock.patch("paperless.consumers.StatusConsumer._authenticated")
|
||||
async def test_close_on_no_auth(self, _authenticated, mock_close):
|
||||
_authenticated.return_value = True
|
||||
|
||||
communicator = WebsocketCommunicator(application, "/ws/status/")
|
||||
connected, subprotocol = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
|
||||
message = {"type": "status_update", "data": {"task_id": "test"}}
|
||||
|
||||
_authenticated.return_value = False
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"status_updates",
|
||||
message,
|
||||
)
|
||||
await communicator.receive_nothing()
|
||||
|
||||
mock_close.assert_called_once()
|
||||
mock_close.reset_mock()
|
||||
|
||||
message = {"type": "documents_deleted", "data": {"documents": [1, 2, 3]}}
|
||||
|
||||
await channel_layer.group_send(
|
||||
"status_updates",
|
||||
message,
|
||||
)
|
||||
await communicator.receive_nothing()
|
||||
|
||||
mock_close.assert_called_once()
|
||||
|
||||
@mock.patch("paperless.consumers.StatusConsumer._authenticated")
|
||||
async def test_auth(self, _authenticated):
|
||||
_authenticated.return_value = True
|
||||
@@ -33,19 +69,19 @@ class TestWebSockets(TestCase):
|
||||
await communicator.disconnect()
|
||||
|
||||
@mock.patch("paperless.consumers.StatusConsumer._authenticated")
|
||||
async def test_receive(self, _authenticated):
|
||||
async def test_receive_status_update(self, _authenticated):
|
||||
_authenticated.return_value = True
|
||||
|
||||
communicator = WebsocketCommunicator(application, "/ws/status/")
|
||||
connected, subprotocol = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
|
||||
message = {"task_id": "test"}
|
||||
message = {"type": "status_update", "data": {"task_id": "test"}}
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"status_updates",
|
||||
{"type": "status_update", "data": message},
|
||||
message,
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
@@ -53,3 +89,73 @@ class TestWebSockets(TestCase):
|
||||
self.assertEqual(response, message)
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
@mock.patch("paperless.consumers.StatusConsumer._authenticated")
|
||||
async def test_receive_documents_deleted(self, _authenticated):
|
||||
_authenticated.return_value = True
|
||||
|
||||
communicator = WebsocketCommunicator(application, "/ws/status/")
|
||||
connected, subprotocol = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
|
||||
message = {"type": "documents_deleted", "data": {"documents": [1, 2, 3]}}
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"status_updates",
|
||||
message,
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
|
||||
self.assertEqual(response, message)
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
@mock.patch("channels.layers.InMemoryChannelLayer.group_send")
|
||||
def test_manager_send_progress(self, mock_group_send):
|
||||
with ProgressManager(task_id="test") as manager:
|
||||
manager.send_progress(
|
||||
ProgressStatusOptions.STARTED,
|
||||
"Test message",
|
||||
1,
|
||||
10,
|
||||
extra_args={
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
message = mock_group_send.call_args[0][1]
|
||||
|
||||
self.assertEqual(
|
||||
message,
|
||||
{
|
||||
"type": "status_update",
|
||||
"data": {
|
||||
"filename": None,
|
||||
"task_id": "test",
|
||||
"current_progress": 1,
|
||||
"max_progress": 10,
|
||||
"status": ProgressStatusOptions.STARTED,
|
||||
"message": "Test message",
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("channels.layers.InMemoryChannelLayer.group_send")
|
||||
def test_manager_send_documents_deleted(self, mock_group_send):
|
||||
with DocumentsStatusManager() as manager:
|
||||
manager.send_documents_deleted([1, 2, 3])
|
||||
|
||||
message = mock_group_send.call_args[0][1]
|
||||
|
||||
self.assertEqual(
|
||||
message,
|
||||
{
|
||||
"type": "documents_deleted",
|
||||
"data": {
|
||||
"documents": [1, 2, 3],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user