mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
Merge remote-tracking branch 'origin/dev' into feature-multiple-barcode-scanners
This commit is contained in:
commit
66929a9088
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -16,7 +16,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
# This is the version of pipenv all the steps will use
|
# This is the version of pipenv all the steps will use
|
||||||
# If changing this, change Dockerfile
|
# If changing this, change Dockerfile
|
||||||
DEFAULT_PIP_ENV_VERSION: "2022.11.30"
|
DEFAULT_PIP_ENV_VERSION: "2023.3.20"
|
||||||
# This is the default version of Python to use in most steps
|
# This is the default version of Python to use in most steps
|
||||||
# If changing this, change Dockerfile
|
# If changing this, change Dockerfile
|
||||||
DEFAULT_PYTHON_VERSION: "3.9"
|
DEFAULT_PYTHON_VERSION: "3.9"
|
||||||
|
23
Dockerfile
23
Dockerfile
@ -29,7 +29,7 @@ COPY Pipfile* ./
|
|||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Installing pipenv" \
|
&& echo "Installing pipenv" \
|
||||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2022.11.30 \
|
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.3.20 \
|
||||||
&& echo "Generating requirement.txt" \
|
&& echo "Generating requirement.txt" \
|
||||||
&& pipenv requirements > requirements.txt
|
&& pipenv requirements > requirements.txt
|
||||||
|
|
||||||
@ -46,15 +46,6 @@ LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-n
|
|||||||
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
# Buildx provided, must be defined to use though
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG TARGETVARIANT
|
|
||||||
|
|
||||||
# Workflow provided
|
|
||||||
ARG JBIG2ENC_VERSION
|
|
||||||
ARG QPDF_VERSION
|
|
||||||
ARG PIKEPDF_VERSION
|
|
||||||
ARG PSYCOPG2_VERSION
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Begin installation and configuration
|
# Begin installation and configuration
|
||||||
@ -175,12 +166,22 @@ RUN set -eux \
|
|||||||
&& chmod +x install_management_commands.sh \
|
&& chmod +x install_management_commands.sh \
|
||||||
&& ./install_management_commands.sh
|
&& ./install_management_commands.sh
|
||||||
|
|
||||||
|
# Buildx provided, must be defined to use though
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
|
||||||
|
# Workflow provided, defaults set for manual building
|
||||||
|
ARG JBIG2ENC_VERSION=0.29
|
||||||
|
ARG QPDF_VERSION=11.3.0
|
||||||
|
ARG PIKEPDF_VERSION=7.1.1
|
||||||
|
ARG PSYCOPG2_VERSION=2.9.5
|
||||||
|
|
||||||
# Install the built packages from the installer library images
|
# Install the built packages from the installer library images
|
||||||
# These change sometimes
|
# These change sometimes
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Getting binaries" \
|
&& echo "Getting binaries" \
|
||||||
&& mkdir paperless-ngx \
|
&& mkdir paperless-ngx \
|
||||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/paperless-ngx/archive/40895f1cdb7702d8cd4b9c3b1d1d7a886018ccd2.tar.gz \
|
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/paperless-ngx/archive/ba28a1e16c27d121b644b4f6bdb78855a2850561.tar.gz \
|
||||||
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
|
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
|
||||||
&& cd paperless-ngx \
|
&& cd paperless-ngx \
|
||||||
# Setting a specific revision ensures we know what this installed
|
# Setting a specific revision ensures we know what this installed
|
||||||
|
6
Pipfile
6
Pipfile
@ -46,6 +46,7 @@ tika = "*"
|
|||||||
# TODO: This will sadly also install daphne+dependencies,
|
# TODO: This will sadly also install daphne+dependencies,
|
||||||
# which an ASGI server we don't need. Adds about 15MB image size.
|
# which an ASGI server we don't need. Adds about 15MB image size.
|
||||||
channels = "~=3.0"
|
channels = "~=3.0"
|
||||||
|
channels-redis = "*"
|
||||||
uvicorn = {extras = ["standard"], version = "*"}
|
uvicorn = {extras = ["standard"], version = "*"}
|
||||||
concurrent-log-handler = "*"
|
concurrent-log-handler = "*"
|
||||||
"pdfminer.six" = "*"
|
"pdfminer.six" = "*"
|
||||||
@ -57,15 +58,12 @@ nltk = "*"
|
|||||||
pdf2image = "*"
|
pdf2image = "*"
|
||||||
flower = "*"
|
flower = "*"
|
||||||
bleach = "*"
|
bleach = "*"
|
||||||
|
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||||
#
|
#
|
||||||
# Packages locked due to issues (try to check if these are fixed in a release every so often)
|
# Packages locked due to issues (try to check if these are fixed in a release every so often)
|
||||||
#
|
#
|
||||||
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
||||||
scipy = "==1.8.1"
|
scipy = "==1.8.1"
|
||||||
# Locked version until https://github.com/django/channels_redis/issues/332
|
|
||||||
# is resolved
|
|
||||||
channels-redis = "==3.4.1"
|
|
||||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
|
555
Pipfile.lock
generated
555
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "d813537b3e32ac288b7a89f85041b1b52b4bf69b349dd0df4a1283dc17ce2275"
|
"sha256": "8395f25f876a71a7307a55dd542e69a4cdcb3be3be38c4e89ed06ce3d52a5345"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
@ -19,13 +19,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"aioredis": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
|
|
||||||
"sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
|
|
||||||
],
|
|
||||||
"version": "==1.3.1"
|
|
||||||
},
|
|
||||||
"amqp": {
|
"amqp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2",
|
"sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2",
|
||||||
@ -55,7 +48,7 @@
|
|||||||
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
|
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
|
||||||
"sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"
|
"sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version <= '3.11.2'",
|
||||||
"version": "==4.0.2"
|
"version": "==4.0.2"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
@ -302,11 +295,11 @@
|
|||||||
},
|
},
|
||||||
"channels-redis": {
|
"channels-redis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:78e4a2f2b2a744fe5a87848ec36b5ee49f522c6808cefe6c583663d0d531faa8",
|
"sha256:122414f29f525f7b9e0c9d59cdcfc4dc1b0eecba16fbb6a1c23f1d9b58f49dcb",
|
||||||
"sha256:ba7e2ad170f273c372812dd32aaac102d68d4e508172abb1cfda3160b7333890"
|
"sha256:81b59d68f53313e1aa891f23591841b684abb936b42e4d1a966d9e4dc63a95ec"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.4.1"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"charset-normalizer": {
|
"charset-normalizer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -481,11 +474,11 @@
|
|||||||
},
|
},
|
||||||
"dateparser": {
|
"dateparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:fbed8b738a24c9cd7f47c4f2089527926566fe539e1a06125eddba75917b1eef",
|
"sha256:070b29b5bbf4b1ec2cd51c96ea040dc68a614de703910a91ad1abba18f9f379f",
|
||||||
"sha256:ff047d9cffad4d3113ead8ec0faf8a7fc43bab7d853ac8715e071312b53c465a"
|
"sha256:86b8b7517efcc558f085a142cdb7620f0921543fcabdb538c8a4c4001d8178e3"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.1.7"
|
"version": "==1.1.8"
|
||||||
},
|
},
|
||||||
"deprecation": {
|
"deprecation": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -576,11 +569,11 @@
|
|||||||
},
|
},
|
||||||
"filelock": {
|
"filelock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4427cdda14a1c68e264845142842d6de2d0fa2c15ba31571a3d9c9a1ec9d191c",
|
"sha256:75997740323c5f12e18f10b494bc11c03e42843129f980f17c04352cc7b09d40",
|
||||||
"sha256:e393782f76abea324dee598d2ea145b857a20df0e0ee4f80fcf35e72a341d2c7"
|
"sha256:eb8f0f2d37ed68223ea63e3bddf2fac99667e4362c88b3f762e434d160190d18"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.9.1"
|
"version": "==3.10.2"
|
||||||
},
|
},
|
||||||
"flower": {
|
"flower": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1053,11 +1046,11 @@
|
|||||||
},
|
},
|
||||||
"ocrmypdf": {
|
"ocrmypdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8fab75052bf77c3488acd9c3054423d9f1f7650e302960a1fa2e991f36c2a66a",
|
"sha256:779b6f77ece5836b4ac703ba02a4bb0ccb758dbb9b4dad1feab3fccd4dba33cf",
|
||||||
"sha256:db03cdd1a5d277fa038b0420ba05fcf7b1f92729ba85431344844ebf01035160"
|
"sha256:c731bd3b6bfd67dc495edc97946f159ba99631854bf7671c2d35c36f30b3ffa8"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==14.0.3"
|
"version": "==14.0.4"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1515,105 +1508,77 @@
|
|||||||
"hiredis"
|
"hiredis"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1eec3741cda408d3a5f84b78d089c8b8d895f21b3b050988351e925faf202864",
|
"sha256:56732e156fe31801c4f43396bd3ca0c2a7f6f83d7936798531b9848d103381aa",
|
||||||
"sha256:5deb072d26e67d2be1712603bfb7947ec3431fb0eec9c578994052e33035af6d"
|
"sha256:7df17a0a2b72a4c8895b462dd07616c51b1dcb48fdd7ecb7b6f4bf39ecb2e94e"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.5.1"
|
"version": "==4.5.3"
|
||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad",
|
"sha256:0a2a851d0548a4e298d88e3ceeb4bad4aab751cf1883edf6150f25718ce0207a",
|
||||||
"sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4",
|
"sha256:148ad520f41021b97870e9c80420e6cdaadcc5e4306e613aed84cd5d53f8a7ca",
|
||||||
"sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd",
|
"sha256:159c7b83488a056365119ada0bceddc06a455d3db7a7aa3cf07f13b2878b885f",
|
||||||
"sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc",
|
"sha256:1937946dd03818845bd9c1713dfd3173a7b9a324e6593a235fc8c51c9cd460eb",
|
||||||
"sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d",
|
"sha256:20ce96da2093e72e151d6af8217a629aeb5f48f1ac543c2fffd1d87c57699d7e",
|
||||||
"sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066",
|
"sha256:24242e5f26823e95edd64969bd206d4752c1a56a744d8cbcf58461f9788bc0c7",
|
||||||
"sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec",
|
"sha256:2e2e6baf4a1108f84966f44870b26766d8f6d104c9959aae329078327c677122",
|
||||||
"sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9",
|
"sha256:328a70e578f37f59eb54e8450b5042190bbadf2ef7f5c0b60829574b62955ed7",
|
||||||
"sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e",
|
"sha256:3371975b165c1e859e1990e5069e8606f00b25aed961cfd25b7bac626b1eb5a9",
|
||||||
"sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8",
|
"sha256:33bab9c9af936123b70b9874ce83f2bcd54be76b97637b33d31560fba8ad5d78",
|
||||||
"sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e",
|
"sha256:33c887b658afb144cdc8ce9156a0e1098453060c18b8bd5177f831ad58e0d60d",
|
||||||
"sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783",
|
"sha256:3582db55372eaee9e998d378109c4b9b15beb2c84624c767efe351363fada9c4",
|
||||||
"sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6",
|
"sha256:3b4da28d89527572f0d4a24814e353e1228a7aeda965e5d9265c1435a154b17a",
|
||||||
"sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1",
|
"sha256:3c4fa90fd91cc2957e66195ce374331bebbc816964864f64b42bd14bda773b53",
|
||||||
"sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c",
|
"sha256:3e66cfc915f5f7e2c8a0af8a27f87aa857f440de7521fd7f2682e23f082142a1",
|
||||||
"sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4",
|
"sha256:3f6f29cb134d782685f8eda01d72073c483c7f87b318b5101c7001faef7850f5",
|
||||||
"sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1",
|
"sha256:43469c22fcf705a7cb59c7e01d6d96975bdbc54c1138900f04d11496489a0054",
|
||||||
"sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1",
|
"sha256:4ad467524cb6879ce42107cf02a49cdb4a06f07fe0e5f1160d7db865a8d25d4b",
|
||||||
"sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7",
|
"sha256:4c9c3db90acd17e4231344a23616f33fd79837809584ce30e2450ca312fa47aa",
|
||||||
"sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8",
|
"sha256:533ba64d67d882286557106a1c5f12b4c2825f11b47a7c209a8c22922ca882be",
|
||||||
"sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe",
|
"sha256:548257463696daf919d2fdfc53ee4b98e29e3ffc5afddd713d83aa849d1fa178",
|
||||||
"sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d",
|
"sha256:55f907c4d18a5a40da0ceb339a0beda77c9df47c934adad987793632fb4318c3",
|
||||||
"sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b",
|
"sha256:5826e7fb443acb49f64f9648a2852efc8d9af2f4c67f6c3dca69dccd9e8e1d15",
|
||||||
"sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8",
|
"sha256:59a15c2803c20702d7f2077807d9a2b7d9a168034b87fd3f0d8361de60019a1e",
|
||||||
"sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c",
|
"sha256:59b3aab231c27cd754d6452c43b12498d34e7ab87d69a502bd0220f4b1c090c4",
|
||||||
"sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af",
|
"sha256:5da83c964aecb6c3f2a6c9a03f3d0fa579e1ad208e2c264ba826cecd19da11fa",
|
||||||
"sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49",
|
"sha256:60b545806a433cc752b9fa936f1c0a63bf96a3872965b958b35bd0d5d788d411",
|
||||||
"sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714",
|
"sha256:60fcef5c3144d861b623456d87ca7fff7af59a4a918e1364cdd0687b48285285",
|
||||||
"sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542",
|
"sha256:617d101b95151d827d5366e9c4225a68c64d56065e41ab9c7ef51bb87f347a8a",
|
||||||
"sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318",
|
"sha256:68e9add923bda8357e6fe65a568766feae369063cb7210297067675cce65272f",
|
||||||
"sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e",
|
"sha256:7798b3d662f70cea425637c54da30ef1894d426cab24ee7ffaaccb24a8b17bb8",
|
||||||
"sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5",
|
"sha256:80a288b21b17e39fb3630cf1d14fd704499bb11d9c8fc110662a0c57758d3d3e",
|
||||||
"sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc",
|
"sha256:81291006a934052161eae8340e7731ea6b8595b0c27dd4927c4e8a489e1760e2",
|
||||||
"sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144",
|
"sha256:8527ea0978ed6dc58ccb3935bd2883537b455c97ec44b5d8084677dfa817f96b",
|
||||||
"sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453",
|
"sha256:87016850c13082747bd120558e6750746177bd492b103b2fca761c8a1c43fba9",
|
||||||
"sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5",
|
"sha256:88552925fd22320600c59ee80342d6eb06bfa9503c3a402d7327983f5fa999d9",
|
||||||
"sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61",
|
"sha256:8d7477ebaf5d3621c763702e1ec0daeede8863fb22459c5e26ddfd17e9b1999c",
|
||||||
"sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11",
|
"sha256:97326d62255203c6026896d4b1ad6b5a0141ba097cae00ed3a508fe454e96baf",
|
||||||
"sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a",
|
"sha256:a4c7b8c5a3a186b49415af3be18e4b8f93b33d6853216c0a1d7401736b703bce",
|
||||||
"sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54",
|
"sha256:aff7c778d9229d66f716ad98a701fa91cf97935ae4a32a145ae9e61619906aaa",
|
||||||
"sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73",
|
"sha256:b280cb303fed94199f0b976595af71ebdcd388fb5e377a8198790f1016a23476",
|
||||||
"sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc",
|
"sha256:b59233cb8df6b60fff5f3056f6f342a8f5f04107a11936bf49ebff87dd4ace34",
|
||||||
"sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347",
|
"sha256:bdab2c90665b88faf5cc5e11bf835d548f4b8d8060c89fc70782b6020850aa1c",
|
||||||
"sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c",
|
"sha256:c00c357a4914f58398503c7f716cf1646b1e36b8176efa35255f5ebfacedfa46",
|
||||||
"sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66",
|
"sha256:c95a977cfdccb8ddef95ddd77cf586fe9dc327c7c93cf712983cece70cdaa1be",
|
||||||
"sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c",
|
"sha256:cdd3d2df486c9a8c6d08f78bdfa8ea7cf6191e037fde38c2cf6f5f0559e9d353",
|
||||||
"sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93",
|
"sha256:d15a0cc48f7a3055e89df1bd6623a907c407d1f58f67ff47064e598d4a550de4",
|
||||||
"sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443",
|
"sha256:d40cecf4bcb2cb37c59e3c79e5bbc45d47e3f3e07edf24e35fc5775db2570058",
|
||||||
"sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc",
|
"sha256:d4d3571c8eb21f0fbe9f0b21b49092c24d442f9a295f079949df3551b2886f29",
|
||||||
"sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1",
|
"sha256:d94a0d25e517c76c9ce9e2e2635d9d1a644b894f466a66a10061f4e599cdc019",
|
||||||
"sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892",
|
"sha256:dcc5b0d6a94637c071a427dc4469efd0ae4fda8ff384790bc8b5baaf9308dc3e",
|
||||||
"sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8",
|
"sha256:e00b046000b313ffaa2f6e8d7290b33b08d2005150eff4c8cf3ad74d011888d1",
|
||||||
"sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001",
|
"sha256:e1b56dac5e86ab52e0443d63b02796357202a8f8c5966b69f8d4c03a94778e98",
|
||||||
"sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa",
|
"sha256:e30d9a6fd7a7a6a4da6f80d167ce8eda4a993ff24282cbc73f34186c46a498db",
|
||||||
"sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90",
|
"sha256:f1977c1fe28173f2349d42c59f80f10a97ce34f2bedb7b7f55e2e8a8de9b7dfb",
|
||||||
"sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c",
|
"sha256:f2bc8a9076ea7add860d57dbee0554a212962ecf2a900344f2fc7c56a02463b0",
|
||||||
"sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0",
|
"sha256:f311ca33fcb9f8fb060c1fa76238d8d029f33b71a2021bafa5d423cc25965b54",
|
||||||
"sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692",
|
"sha256:f579a202b90c1110d0894a86b32a89bf550fdb34bdd3f9f550115706be462e19",
|
||||||
"sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4",
|
"sha256:fa41a427d4f03ec6d6da2fd8a230f4f388f336cd7ca46b46c4d2a1bca3ead85a",
|
||||||
"sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5",
|
"sha256:fd47362e03acc780aad5a5bc4624d495594261b55a1f79a5b775b6be865a5911"
|
||||||
"sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690",
|
|
||||||
"sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83",
|
|
||||||
"sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66",
|
|
||||||
"sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f",
|
|
||||||
"sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f",
|
|
||||||
"sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4",
|
|
||||||
"sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee",
|
|
||||||
"sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81",
|
|
||||||
"sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95",
|
|
||||||
"sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9",
|
|
||||||
"sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff",
|
|
||||||
"sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e",
|
|
||||||
"sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5",
|
|
||||||
"sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6",
|
|
||||||
"sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7",
|
|
||||||
"sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1",
|
|
||||||
"sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394",
|
|
||||||
"sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6",
|
|
||||||
"sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742",
|
|
||||||
"sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57",
|
|
||||||
"sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b",
|
|
||||||
"sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7",
|
|
||||||
"sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b",
|
|
||||||
"sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244",
|
|
||||||
"sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af",
|
|
||||||
"sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185",
|
|
||||||
"sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8",
|
|
||||||
"sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"
|
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2022.10.31"
|
"version": "==2023.3.22"
|
||||||
},
|
},
|
||||||
"reportlab": {
|
"reportlab": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1925,11 +1890,11 @@
|
|||||||
},
|
},
|
||||||
"tzlocal": {
|
"tzlocal": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745",
|
"sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355",
|
||||||
"sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"
|
"sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==4.2"
|
"version": "==4.3"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1944,11 +1909,11 @@
|
|||||||
"standard"
|
"standard"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8635a388062222082f4b06225b867b74a7e4ef942124453d4d1d1a5cb3750932",
|
"sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032",
|
||||||
"sha256:e69e955cb621ae7b75f5590a814a4fcbfb14cb8f44a36dfe3c5c75ab8aee3ad5"
|
"sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.21.0"
|
"version": "==0.21.1"
|
||||||
},
|
},
|
||||||
"uvloop": {
|
"uvloop": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2165,45 +2130,39 @@
|
|||||||
},
|
},
|
||||||
"zope.interface": {
|
"zope.interface": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32",
|
"sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373",
|
||||||
"sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0",
|
"sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb",
|
||||||
"sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c",
|
"sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446",
|
||||||
"sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c",
|
"sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8",
|
||||||
"sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d",
|
"sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c",
|
||||||
"sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf",
|
"sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8",
|
||||||
"sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b",
|
"sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2",
|
||||||
"sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc",
|
"sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f",
|
||||||
"sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f",
|
"sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f",
|
||||||
"sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d",
|
"sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5",
|
||||||
"sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e",
|
"sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85",
|
||||||
"sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16",
|
"sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc",
|
||||||
"sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f",
|
"sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788",
|
||||||
"sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9",
|
"sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518",
|
||||||
"sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296",
|
"sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410",
|
||||||
"sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a",
|
"sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464",
|
||||||
"sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d",
|
"sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5",
|
||||||
"sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d",
|
"sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d",
|
||||||
"sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189",
|
"sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52",
|
||||||
"sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4",
|
"sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca",
|
||||||
"sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452",
|
"sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8",
|
||||||
"sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a",
|
"sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2",
|
||||||
"sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0",
|
"sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f",
|
||||||
"sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5",
|
"sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58",
|
||||||
"sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671",
|
"sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a",
|
||||||
"sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e",
|
"sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d",
|
||||||
"sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f",
|
"sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28",
|
||||||
"sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396",
|
"sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990",
|
||||||
"sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7",
|
"sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995",
|
||||||
"sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b",
|
"sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30"
|
||||||
"sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf",
|
|
||||||
"sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f",
|
|
||||||
"sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6",
|
|
||||||
"sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188",
|
|
||||||
"sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7",
|
|
||||||
"sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b"
|
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==5.5.2"
|
"version": "==6.0"
|
||||||
},
|
},
|
||||||
"zstandard": {
|
"zstandard": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2542,19 +2501,19 @@
|
|||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:51f37ff9df710159d6d736d0ba1c75e063430a8c806b91334d7794305b5a6114",
|
"sha256:2deeee8fed3d1b8ae5f87d172d4569ddc859aab8693f7cd68eddc5d20400563a",
|
||||||
"sha256:5aaa16fa9cfde7d117eef70b6b293a705021e57158f3fa6b44ed1b70202d2065"
|
"sha256:e7c058e1f360f245f265625b32d3189d7229398ad80a8b6bac459891745de052"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==17.6.0"
|
"version": "==18.3.0"
|
||||||
},
|
},
|
||||||
"filelock": {
|
"filelock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4427cdda14a1c68e264845142842d6de2d0fa2c15ba31571a3d9c9a1ec9d191c",
|
"sha256:75997740323c5f12e18f10b494bc11c03e42843129f980f17c04352cc7b09d40",
|
||||||
"sha256:e393782f76abea324dee598d2ea145b857a20df0e0ee4f80fcf35e72a341d2c7"
|
"sha256:eb8f0f2d37ed68223ea63e3bddf2fac99667e4362c88b3f762e434d160190d18"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.9.1"
|
"version": "==3.10.2"
|
||||||
},
|
},
|
||||||
"ghp-import": {
|
"ghp-import": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2565,11 +2524,11 @@
|
|||||||
},
|
},
|
||||||
"identify": {
|
"identify": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5dfef8a745ca4f2c95f27e9db74cb4c8b6d9916383988e8791f3595868f78a33",
|
"sha256:69edcaffa8e91ae0f77d397af60f148b6b45a8044b2cc6d99cafa5b04793ff00",
|
||||||
"sha256:c8b288552bc5f05a08aff09af2f58e6976bf8ac87beb38498a0e3d98ba64eb18"
|
"sha256:7671a05ef9cfaf8ff63b15d45a91a1147a03aaccb2976d4e9bd047cbbc508471"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.5.20"
|
"version": "==2.5.21"
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2589,11 +2548,11 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
|
"sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20",
|
||||||
"sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
|
"sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.10'",
|
"markers": "python_version < '3.10'",
|
||||||
"version": "==6.0.0"
|
"version": "==6.1.0"
|
||||||
},
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2767,11 +2726,11 @@
|
|||||||
},
|
},
|
||||||
"pathspec": {
|
"pathspec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229",
|
"sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687",
|
||||||
"sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"
|
"sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==0.11.0"
|
"version": "==0.11.1"
|
||||||
},
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2874,11 +2833,11 @@
|
|||||||
},
|
},
|
||||||
"pre-commit": {
|
"pre-commit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b80254e60668e1dd1f5c03a1c9e0413941d61f568a57d745add265945f65bfe8",
|
"sha256:818f0d998059934d0f81bb3667e3ccdc32da6ed7ccaac33e43dc231561ddaaa9",
|
||||||
"sha256:d63e6537f9252d99f65755ae5b79c989b462d511ebbc481b561db6a297e1e865"
|
"sha256:f712d3688102e13c8e66b7d7dbd8934a6dda157e58635d89f7d6fecdca39ce8a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.1"
|
"version": "==3.2.0"
|
||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3038,97 +2997,69 @@
|
|||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad",
|
"sha256:0a2a851d0548a4e298d88e3ceeb4bad4aab751cf1883edf6150f25718ce0207a",
|
||||||
"sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4",
|
"sha256:148ad520f41021b97870e9c80420e6cdaadcc5e4306e613aed84cd5d53f8a7ca",
|
||||||
"sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd",
|
"sha256:159c7b83488a056365119ada0bceddc06a455d3db7a7aa3cf07f13b2878b885f",
|
||||||
"sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc",
|
"sha256:1937946dd03818845bd9c1713dfd3173a7b9a324e6593a235fc8c51c9cd460eb",
|
||||||
"sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d",
|
"sha256:20ce96da2093e72e151d6af8217a629aeb5f48f1ac543c2fffd1d87c57699d7e",
|
||||||
"sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066",
|
"sha256:24242e5f26823e95edd64969bd206d4752c1a56a744d8cbcf58461f9788bc0c7",
|
||||||
"sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec",
|
"sha256:2e2e6baf4a1108f84966f44870b26766d8f6d104c9959aae329078327c677122",
|
||||||
"sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9",
|
"sha256:328a70e578f37f59eb54e8450b5042190bbadf2ef7f5c0b60829574b62955ed7",
|
||||||
"sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e",
|
"sha256:3371975b165c1e859e1990e5069e8606f00b25aed961cfd25b7bac626b1eb5a9",
|
||||||
"sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8",
|
"sha256:33bab9c9af936123b70b9874ce83f2bcd54be76b97637b33d31560fba8ad5d78",
|
||||||
"sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e",
|
"sha256:33c887b658afb144cdc8ce9156a0e1098453060c18b8bd5177f831ad58e0d60d",
|
||||||
"sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783",
|
"sha256:3582db55372eaee9e998d378109c4b9b15beb2c84624c767efe351363fada9c4",
|
||||||
"sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6",
|
"sha256:3b4da28d89527572f0d4a24814e353e1228a7aeda965e5d9265c1435a154b17a",
|
||||||
"sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1",
|
"sha256:3c4fa90fd91cc2957e66195ce374331bebbc816964864f64b42bd14bda773b53",
|
||||||
"sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c",
|
"sha256:3e66cfc915f5f7e2c8a0af8a27f87aa857f440de7521fd7f2682e23f082142a1",
|
||||||
"sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4",
|
"sha256:3f6f29cb134d782685f8eda01d72073c483c7f87b318b5101c7001faef7850f5",
|
||||||
"sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1",
|
"sha256:43469c22fcf705a7cb59c7e01d6d96975bdbc54c1138900f04d11496489a0054",
|
||||||
"sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1",
|
"sha256:4ad467524cb6879ce42107cf02a49cdb4a06f07fe0e5f1160d7db865a8d25d4b",
|
||||||
"sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7",
|
"sha256:4c9c3db90acd17e4231344a23616f33fd79837809584ce30e2450ca312fa47aa",
|
||||||
"sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8",
|
"sha256:533ba64d67d882286557106a1c5f12b4c2825f11b47a7c209a8c22922ca882be",
|
||||||
"sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe",
|
"sha256:548257463696daf919d2fdfc53ee4b98e29e3ffc5afddd713d83aa849d1fa178",
|
||||||
"sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d",
|
"sha256:55f907c4d18a5a40da0ceb339a0beda77c9df47c934adad987793632fb4318c3",
|
||||||
"sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b",
|
"sha256:5826e7fb443acb49f64f9648a2852efc8d9af2f4c67f6c3dca69dccd9e8e1d15",
|
||||||
"sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8",
|
"sha256:59a15c2803c20702d7f2077807d9a2b7d9a168034b87fd3f0d8361de60019a1e",
|
||||||
"sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c",
|
"sha256:59b3aab231c27cd754d6452c43b12498d34e7ab87d69a502bd0220f4b1c090c4",
|
||||||
"sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af",
|
"sha256:5da83c964aecb6c3f2a6c9a03f3d0fa579e1ad208e2c264ba826cecd19da11fa",
|
||||||
"sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49",
|
"sha256:60b545806a433cc752b9fa936f1c0a63bf96a3872965b958b35bd0d5d788d411",
|
||||||
"sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714",
|
"sha256:60fcef5c3144d861b623456d87ca7fff7af59a4a918e1364cdd0687b48285285",
|
||||||
"sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542",
|
"sha256:617d101b95151d827d5366e9c4225a68c64d56065e41ab9c7ef51bb87f347a8a",
|
||||||
"sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318",
|
"sha256:68e9add923bda8357e6fe65a568766feae369063cb7210297067675cce65272f",
|
||||||
"sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e",
|
"sha256:7798b3d662f70cea425637c54da30ef1894d426cab24ee7ffaaccb24a8b17bb8",
|
||||||
"sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5",
|
"sha256:80a288b21b17e39fb3630cf1d14fd704499bb11d9c8fc110662a0c57758d3d3e",
|
||||||
"sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc",
|
"sha256:81291006a934052161eae8340e7731ea6b8595b0c27dd4927c4e8a489e1760e2",
|
||||||
"sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144",
|
"sha256:8527ea0978ed6dc58ccb3935bd2883537b455c97ec44b5d8084677dfa817f96b",
|
||||||
"sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453",
|
"sha256:87016850c13082747bd120558e6750746177bd492b103b2fca761c8a1c43fba9",
|
||||||
"sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5",
|
"sha256:88552925fd22320600c59ee80342d6eb06bfa9503c3a402d7327983f5fa999d9",
|
||||||
"sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61",
|
"sha256:8d7477ebaf5d3621c763702e1ec0daeede8863fb22459c5e26ddfd17e9b1999c",
|
||||||
"sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11",
|
"sha256:97326d62255203c6026896d4b1ad6b5a0141ba097cae00ed3a508fe454e96baf",
|
||||||
"sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a",
|
"sha256:a4c7b8c5a3a186b49415af3be18e4b8f93b33d6853216c0a1d7401736b703bce",
|
||||||
"sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54",
|
"sha256:aff7c778d9229d66f716ad98a701fa91cf97935ae4a32a145ae9e61619906aaa",
|
||||||
"sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73",
|
"sha256:b280cb303fed94199f0b976595af71ebdcd388fb5e377a8198790f1016a23476",
|
||||||
"sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc",
|
"sha256:b59233cb8df6b60fff5f3056f6f342a8f5f04107a11936bf49ebff87dd4ace34",
|
||||||
"sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347",
|
"sha256:bdab2c90665b88faf5cc5e11bf835d548f4b8d8060c89fc70782b6020850aa1c",
|
||||||
"sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c",
|
"sha256:c00c357a4914f58398503c7f716cf1646b1e36b8176efa35255f5ebfacedfa46",
|
||||||
"sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66",
|
"sha256:c95a977cfdccb8ddef95ddd77cf586fe9dc327c7c93cf712983cece70cdaa1be",
|
||||||
"sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c",
|
"sha256:cdd3d2df486c9a8c6d08f78bdfa8ea7cf6191e037fde38c2cf6f5f0559e9d353",
|
||||||
"sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93",
|
"sha256:d15a0cc48f7a3055e89df1bd6623a907c407d1f58f67ff47064e598d4a550de4",
|
||||||
"sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443",
|
"sha256:d40cecf4bcb2cb37c59e3c79e5bbc45d47e3f3e07edf24e35fc5775db2570058",
|
||||||
"sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc",
|
"sha256:d4d3571c8eb21f0fbe9f0b21b49092c24d442f9a295f079949df3551b2886f29",
|
||||||
"sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1",
|
"sha256:d94a0d25e517c76c9ce9e2e2635d9d1a644b894f466a66a10061f4e599cdc019",
|
||||||
"sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892",
|
"sha256:dcc5b0d6a94637c071a427dc4469efd0ae4fda8ff384790bc8b5baaf9308dc3e",
|
||||||
"sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8",
|
"sha256:e00b046000b313ffaa2f6e8d7290b33b08d2005150eff4c8cf3ad74d011888d1",
|
||||||
"sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001",
|
"sha256:e1b56dac5e86ab52e0443d63b02796357202a8f8c5966b69f8d4c03a94778e98",
|
||||||
"sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa",
|
"sha256:e30d9a6fd7a7a6a4da6f80d167ce8eda4a993ff24282cbc73f34186c46a498db",
|
||||||
"sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90",
|
"sha256:f1977c1fe28173f2349d42c59f80f10a97ce34f2bedb7b7f55e2e8a8de9b7dfb",
|
||||||
"sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c",
|
"sha256:f2bc8a9076ea7add860d57dbee0554a212962ecf2a900344f2fc7c56a02463b0",
|
||||||
"sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0",
|
"sha256:f311ca33fcb9f8fb060c1fa76238d8d029f33b71a2021bafa5d423cc25965b54",
|
||||||
"sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692",
|
"sha256:f579a202b90c1110d0894a86b32a89bf550fdb34bdd3f9f550115706be462e19",
|
||||||
"sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4",
|
"sha256:fa41a427d4f03ec6d6da2fd8a230f4f388f336cd7ca46b46c4d2a1bca3ead85a",
|
||||||
"sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5",
|
"sha256:fd47362e03acc780aad5a5bc4624d495594261b55a1f79a5b775b6be865a5911"
|
||||||
"sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690",
|
|
||||||
"sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83",
|
|
||||||
"sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66",
|
|
||||||
"sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f",
|
|
||||||
"sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f",
|
|
||||||
"sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4",
|
|
||||||
"sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee",
|
|
||||||
"sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81",
|
|
||||||
"sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95",
|
|
||||||
"sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9",
|
|
||||||
"sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff",
|
|
||||||
"sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e",
|
|
||||||
"sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5",
|
|
||||||
"sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6",
|
|
||||||
"sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7",
|
|
||||||
"sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1",
|
|
||||||
"sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394",
|
|
||||||
"sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6",
|
|
||||||
"sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742",
|
|
||||||
"sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57",
|
|
||||||
"sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b",
|
|
||||||
"sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7",
|
|
||||||
"sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b",
|
|
||||||
"sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244",
|
|
||||||
"sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af",
|
|
||||||
"sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185",
|
|
||||||
"sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8",
|
|
||||||
"sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"
|
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2022.10.31"
|
"version": "==2023.3.22"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3513,30 +3444,30 @@
|
|||||||
"compatible-mypy"
|
"compatible-mypy"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0bbf9eb172c5b06eccff2d704c7c3906e4a2c6146df8c32ee9f3a51e29265581",
|
"sha256:1bd96207576cd220221a0e615f0259f13d453d515a80f576c1246e0fb547f561",
|
||||||
"sha256:25010658acac0ce4a69211b55dd719fd16dbfe54fcfe5c878d0c8db07bdd5482"
|
"sha256:c95f948e2bfc565f3147e969ff361ef033841a0b8a51cac974a6cc6d0486732c"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.15.0"
|
"version": "==1.16.0"
|
||||||
},
|
},
|
||||||
"django-stubs-ext": {
|
"django-stubs-ext": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4fd8cdbc68d1a421f21bb7e0d9e76d50f6a4b504d350ba786405daf536e90c21",
|
"sha256:9a9ba9e2808737949de96a0fce8b054f12d38e461011d77ebc074ffe8c43dfcb",
|
||||||
"sha256:d729fbc7fe8970a7e26b35956c35b48502516f011d523c0577bdfb02ed956284"
|
"sha256:a454d349d19c26d6c50c4c6dbc1e8af4a9cda4ce1dc4104e3dd4c0330510cc56"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==0.7.0"
|
"version": "==0.8.0"
|
||||||
},
|
},
|
||||||
"djangorestframework-stubs": {
|
"djangorestframework-stubs": {
|
||||||
"extras": [
|
"extras": [
|
||||||
"compatible-mypy"
|
"compatible-mypy"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:89f6c2add193cb5ab61b9e47187b33a93cc099376a8df5e4d6c3fc8ecb992d3b",
|
"sha256:433edd7f10786914138b300b9be5aba1ebc80c471b5156934664afd7e9df9fd6",
|
||||||
"sha256:9475e1374b057ffbdcaaa84a060fe5f01476d8b9014d82a83b4153f57fbcbc1f"
|
"sha256:69e8a1ea7eb815cbe35155c27eee72522d7c8666d3cbdacb9997ab88c7b4202c"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.9.1"
|
"version": "==1.10.0"
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3548,35 +3479,35 @@
|
|||||||
},
|
},
|
||||||
"mypy": {
|
"mypy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6",
|
"sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5",
|
||||||
"sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3",
|
"sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598",
|
||||||
"sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c",
|
"sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5",
|
||||||
"sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262",
|
"sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389",
|
||||||
"sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e",
|
"sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a",
|
||||||
"sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0",
|
"sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9",
|
||||||
"sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d",
|
"sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78",
|
||||||
"sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65",
|
"sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af",
|
||||||
"sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8",
|
"sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f",
|
||||||
"sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76",
|
"sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4",
|
||||||
"sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4",
|
"sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c",
|
||||||
"sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407",
|
"sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2",
|
||||||
"sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4",
|
"sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e",
|
||||||
"sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b",
|
"sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1",
|
||||||
"sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a",
|
"sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51",
|
||||||
"sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf",
|
"sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f",
|
||||||
"sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5",
|
"sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a",
|
||||||
"sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf",
|
"sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54",
|
||||||
"sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd",
|
"sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f",
|
||||||
"sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8",
|
"sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5",
|
||||||
"sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994",
|
"sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707",
|
||||||
"sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff",
|
"sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b",
|
||||||
"sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88",
|
"sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b",
|
||||||
"sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919",
|
"sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c",
|
||||||
"sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6",
|
"sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799",
|
||||||
"sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4"
|
"sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.0.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"mypy-extensions": {
|
"mypy-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3727,26 +3658,26 @@
|
|||||||
},
|
},
|
||||||
"types-redis": {
|
"types-redis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:43d92b4d6315a45bb0e9a790683ba4448ada88cd1233f3f9886fa6f783f53956",
|
"sha256:7c1d5fdb0a2d5fd92eac37ce382fdb47d99a69889e7d6c2bc4479148ac646c73",
|
||||||
"sha256:f516254bd593023110a38b77e80d5a76a7f033f1d94c53bee09a7d5d0433f34d"
|
"sha256:f23415e448ca25ec5028c24fdf3717a13f0c905eb1933733e8a8a7d4952f6908"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.5.1.5"
|
"version": "==4.5.3.0"
|
||||||
},
|
},
|
||||||
"types-requests": {
|
"types-requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a05e4c7bc967518fba5789c341ea8b0c942776ee474c7873129a61161978e586",
|
"sha256:9d4002056df7ebc4ec1f28fd701fba82c5c22549c4477116cb2656aa30ace6db",
|
||||||
"sha256:fc8eaa09cc014699c6b63c60c2e3add0c8b09a410c818b5ac6e65f92a26dde09"
|
"sha256:a86921028335fdcc3aaf676c9d3463f867db6af2303fc65aa309b13ae1e6dd53"
|
||||||
],
|
],
|
||||||
"version": "==2.28.11.15"
|
"version": "==2.28.11.16"
|
||||||
},
|
},
|
||||||
"types-setuptools": {
|
"types-setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:70b5e6a379e9fccf6579871a93ca3301a46252e3ae66957ec64281a2b6a812d9",
|
"sha256:3a708e66c7bdc620e4d0439f344c750c57a4340c895a4c3ed2d0fc4ae8eb9962",
|
||||||
"sha256:d669a80ee8e37eb1697dc31a23d41ea2c48a635464e2c7e6370dda811459b466"
|
"sha256:dae5a4a659dbb6dba57773440f6e2dbdd8ef282dc136a174a8a59bd33d949945"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==67.6.0.0"
|
"version": "==67.6.0.5"
|
||||||
},
|
},
|
||||||
"types-tqdm": {
|
"types-tqdm": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -80,7 +80,7 @@ django_checks() {
|
|||||||
|
|
||||||
search_index() {
|
search_index() {
|
||||||
|
|
||||||
local -r index_version=3
|
local -r index_version=4
|
||||||
local -r index_version_file=${DATA_DIR}/.index_version
|
local -r index_version_file=${DATA_DIR}/.index_version
|
||||||
|
|
||||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||||
|
@ -28,7 +28,7 @@ stderr_logfile_maxbytes=0
|
|||||||
|
|
||||||
[program:celery]
|
[program:celery]
|
||||||
|
|
||||||
command = celery --app paperless worker --loglevel INFO
|
command = celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
|
||||||
user=paperless
|
user=paperless
|
||||||
stopasgroup = true
|
stopasgroup = true
|
||||||
stopwaitsecs = 60
|
stopwaitsecs = 60
|
||||||
|
@ -475,12 +475,13 @@ mail_fetcher
|
|||||||
The command takes no arguments and processes all your mail accounts and
|
The command takes no arguments and processes all your mail accounts and
|
||||||
rules.
|
rules.
|
||||||
|
|
||||||
!!! note
|
!!! tip
|
||||||
|
|
||||||
As of October 2022 Microsoft no longer supports IMAP authentication
|
To use OAuth access tokens for mail fetching,
|
||||||
for Exchange servers, thus Exchange is no longer supported until a
|
select the box to indicate the password is actually
|
||||||
solution is implemented in the Python IMAP library used by Paperless.
|
a token when creating or editing a mail account. The
|
||||||
See [learn.microsoft.com](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online)
|
details for creating a token depend on your email
|
||||||
|
provider.
|
||||||
|
|
||||||
### Creating archived documents {#archiver}
|
### Creating archived documents {#archiver}
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 890 KiB |
@ -86,6 +86,36 @@ changed here.
|
|||||||
|
|
||||||
Default is `prefer`.
|
Default is `prefer`.
|
||||||
|
|
||||||
|
`PAPERLESS_DBSSLROOTCERT=<ca-path>`
|
||||||
|
|
||||||
|
: SSL root certificate path
|
||||||
|
|
||||||
|
See [the official documentation about
|
||||||
|
sslmode](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||||
|
Changes path of `root.crt`.
|
||||||
|
|
||||||
|
Defaults to unset, using the documented path in the home directory.
|
||||||
|
|
||||||
|
`PAPERLESS_DBSSLCERT=<client-cert-path>`
|
||||||
|
|
||||||
|
: SSL client certificate path
|
||||||
|
|
||||||
|
See [the official documentation about
|
||||||
|
sslmode](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||||
|
Changes path of `postgresql.crt`.
|
||||||
|
|
||||||
|
Defaults to unset, using the documented path in the home directory.
|
||||||
|
|
||||||
|
`PAPERLESS_DBSSLKEY=<client-cert-key>`
|
||||||
|
|
||||||
|
: SSL client key path
|
||||||
|
|
||||||
|
See [the official documentation about
|
||||||
|
sslmode](https://www.postgresql.org/docs/current/libpq-ssl.html).
|
||||||
|
Changes path of `postgresql.key`.
|
||||||
|
|
||||||
|
Defaults to unset, using the documented path in the home directory.
|
||||||
|
|
||||||
`PAPERLESS_DB_TIMEOUT=<float>`
|
`PAPERLESS_DB_TIMEOUT=<float>`
|
||||||
|
|
||||||
: Amount of time for a database connection to wait for the database to
|
: Amount of time for a database connection to wait for the database to
|
||||||
|
@ -332,3 +332,16 @@ change the port gunicorn listens on.
|
|||||||
|
|
||||||
To fix this, set `PAPERLESS_PORT` again to your desired port, or the
|
To fix this, set `PAPERLESS_PORT` again to your desired port, or the
|
||||||
default of 8000.
|
default of 8000.
|
||||||
|
|
||||||
|
## Database Warns about unique constraint "documents_tag_name_uniq
|
||||||
|
|
||||||
|
You may see database log lines like:
|
||||||
|
|
||||||
|
```
|
||||||
|
ERROR: duplicate key value violates unique constraint "documents_tag_name_uniq"
|
||||||
|
DETAIL: Key (name)=(NameF) already exists.
|
||||||
|
STATEMENT: INSERT INTO "documents_tag" ("owner_id", "name", "match", "matching_algorithm", "is_insensitive", "color", "is_inbox_tag") VALUES (NULL, 'NameF', '', 1, true, '#a6cee3', false) RETURNING "documents_tag"."id"
|
||||||
|
```
|
||||||
|
|
||||||
|
This can happen during heavy consumption when using polling. Paperless will handle it correctly and the file
|
||||||
|
will still be consumed
|
||||||
|
@ -17,28 +17,28 @@ describe('document-detail', () => {
|
|||||||
req.reply({ result: 'OK' })
|
req.reply({ result: 'OK' })
|
||||||
}).as('saveDoc')
|
}).as('saveDoc')
|
||||||
|
|
||||||
cy.fixture('documents/1/comments.json').then((commentsJson) => {
|
cy.fixture('documents/1/notes.json').then((notesJson) => {
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'GET',
|
'GET',
|
||||||
'http://localhost:8000/api/documents/1/comments/',
|
'http://localhost:8000/api/documents/1/notes/',
|
||||||
(req) => {
|
(req) => {
|
||||||
req.reply(commentsJson.filter((c) => c.id != 10)) // 3
|
req.reply(notesJson.filter((c) => c.id != 10)) // 3
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'http://localhost:8000/api/documents/1/comments/?id=9',
|
'http://localhost:8000/api/documents/1/notes/?id=9',
|
||||||
(req) => {
|
(req) => {
|
||||||
req.reply(commentsJson.filter((c) => c.id != 9 && c.id != 10)) // 2
|
req.reply(notesJson.filter((c) => c.id != 9 && c.id != 10)) // 2
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'POST',
|
'POST',
|
||||||
'http://localhost:8000/api/documents/1/comments/',
|
'http://localhost:8000/api/documents/1/notes/',
|
||||||
(req) => {
|
(req) => {
|
||||||
req.reply(commentsJson) // 4
|
req.reply(notesJson) // 4
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -75,33 +75,40 @@ describe('document-detail', () => {
|
|||||||
cy.get('pdf-viewer').should('be.visible')
|
cy.get('pdf-viewer').should('be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show a list of comments', () => {
|
it('should show a list of notes', () => {
|
||||||
cy.wait(1000)
|
cy.wait(1000).get('a').contains('Notes').click({ force: true }).wait(1000)
|
||||||
.get('a')
|
cy.get('app-document-notes').find('.card').its('length').should('eq', 3)
|
||||||
.contains('Comments')
|
|
||||||
.click({ force: true })
|
|
||||||
.wait(1000)
|
|
||||||
cy.get('app-document-comments').find('.card').its('length').should('eq', 3)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support comment deletion', () => {
|
it('should support note deletion', () => {
|
||||||
cy.wait(1000).get('a').contains('Comments').click().wait(1000)
|
cy.wait(1000).get('a').contains('Notes').click().wait(1000)
|
||||||
cy.get('app-document-comments')
|
cy.get('app-document-notes')
|
||||||
.find('.card')
|
.find('.card')
|
||||||
.first()
|
.first()
|
||||||
.find('button')
|
.find('button')
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
.wait(500)
|
.wait(500)
|
||||||
cy.get('app-document-comments').find('.card').its('length').should('eq', 2)
|
cy.get('app-document-notes').find('.card').its('length').should('eq', 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support comment insertion', () => {
|
it('should support note insertion', () => {
|
||||||
cy.wait(1000).get('a').contains('Comments').click().wait(1000)
|
cy.wait(1000).get('a').contains('Notes').click().wait(1000)
|
||||||
cy.get('app-document-comments')
|
cy.get('app-document-notes')
|
||||||
.find('form textarea')
|
.find('form textarea')
|
||||||
.type('Testing new comment')
|
.type('Testing new note')
|
||||||
.wait(500)
|
.wait(500)
|
||||||
cy.get('app-document-comments').find('form button').click().wait(1500)
|
cy.get('app-document-notes').find('form button').click().wait(1500)
|
||||||
cy.get('app-document-comments').find('.card').its('length').should('eq', 4)
|
cy.get('app-document-notes').find('.card').its('length').should('eq', 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support navigation to notes tab by url', () => {
|
||||||
|
cy.visit('/documents/1/notes')
|
||||||
|
cy.get('app-document-notes').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should dynamically update note counts', () => {
|
||||||
|
cy.visit('/documents/1/notes')
|
||||||
|
cy.get('app-document-notes').within(() => cy.contains('Delete').click())
|
||||||
|
cy.get('ul.nav').find('li').contains('Notes').find('.badge').contains('2')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -48,6 +48,26 @@ describe('documents-list', () => {
|
|||||||
(d.tags as Array<number>).includes(tag_id)
|
(d.tags as Array<number>).includes(tag_id)
|
||||||
)
|
)
|
||||||
response.count = response.results.length
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('correspondent__id__in')) {
|
||||||
|
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__in=9,14
|
||||||
|
const correspondent_ids = req.query['correspondent__id__in']
|
||||||
|
.toString()
|
||||||
|
.split(',')
|
||||||
|
.map((c) => +c)
|
||||||
|
response.results = (documentsJson.results as Array<any>).filter((d) =>
|
||||||
|
correspondent_ids.includes(d.correspondent)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('correspondent__id__none')) {
|
||||||
|
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__none=9,14
|
||||||
|
const correspondent_ids = req.query['correspondent__id__none']
|
||||||
|
.toString()
|
||||||
|
.split(',')
|
||||||
|
.map((c) => +c)
|
||||||
|
response.results = (documentsJson.results as Array<any>).filter(
|
||||||
|
(d) => !correspondent_ids.includes(d.correspondent)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
}
|
}
|
||||||
|
|
||||||
req.reply(response)
|
req.reply(response)
|
||||||
@ -112,6 +132,27 @@ describe('documents-list', () => {
|
|||||||
cy.contains('One document')
|
cy.contains('One document')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should filter including multiple correspondents', () => {
|
||||||
|
cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]')
|
||||||
|
.click()
|
||||||
|
.within(() => {
|
||||||
|
cy.contains('button', 'ABC Test Correspondent').click()
|
||||||
|
cy.contains('button', 'Corresp 11').click()
|
||||||
|
})
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter excluding multiple correspondents', () => {
|
||||||
|
cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]')
|
||||||
|
.click()
|
||||||
|
.within(() => {
|
||||||
|
cy.contains('button', 'ABC Test Correspondent').click()
|
||||||
|
cy.contains('button', 'Corresp 11').click()
|
||||||
|
cy.contains('label', 'Exclude').click()
|
||||||
|
})
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
it('should apply tags', () => {
|
it('should apply tags', () => {
|
||||||
cy.get('app-document-card-small:first-of-type').click()
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within(
|
cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within(
|
||||||
|
@ -232,6 +232,11 @@ describe('documents query params', () => {
|
|||||||
|
|
||||||
it('should show a list of documents filtered by document type', () => {
|
it('should show a list of documents filtered by document type', () => {
|
||||||
cy.visit('/documents?sort=created&reverse=true&document_type__id=1')
|
cy.visit('/documents?sort=created&reverse=true&document_type__id=1')
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by multiple correspondents', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&document_type__id__in=1,2')
|
||||||
cy.contains('3 documents')
|
cy.contains('3 documents')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -245,9 +250,14 @@ describe('documents query params', () => {
|
|||||||
cy.contains('2 documents')
|
cy.contains('2 documents')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by multiple correspondents', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&correspondent__id__in=9,14')
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
it('should show a list of documents filtered by no correspondent', () => {
|
it('should show a list of documents filtered by no correspondent', () => {
|
||||||
cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1')
|
cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1')
|
||||||
cy.contains('2 documents')
|
cy.contains('One document')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show a list of documents filtered by storage path', () => {
|
it('should show a list of documents filtered by storage path', () => {
|
||||||
|
@ -1 +1,257 @@
|
|||||||
{"count":27,"next":"http://localhost:8000/api/correspondents/?page=2","previous":null,"results":[{"id":9,"slug":"abc-test-correspondent","name":"ABC Test Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":13,"slug":"corresp-10","name":"Corresp 10","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":14,"slug":"corresp-11","name":"Corresp 11","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":15,"slug":"corresp-12","name":"Corresp 12","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":16,"slug":"corresp-13","name":"Corresp 13","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":18,"slug":"corresp-15","name":"Corresp 15","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":19,"slug":"corresp-16","name":"Corresp 16","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":20,"slug":"corresp-17","name":"Corresp 17","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":21,"slug":"corresp-18","name":"Corresp 18","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":22,"slug":"corresp-19","name":"Corresp 19","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":23,"slug":"corresp-20","name":"Corresp 20","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":24,"slug":"corresp-21","name":"Corresp 21","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":25,"slug":"corresp-22","name":"Corresp 22","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":26,"slug":"corresp-23","name":"Corresp 23","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":5,"slug":"corresp-3","name":"Corresp 3","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":6,"slug":"corresp-4","name":"Corresp 4","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":7,"slug":"corresp-5","name":"Corresp 5","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":8,"slug":"corresp-6","name":"Corresp 6","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":10,"slug":"corresp-7","name":"Corresp 7","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":11,"slug":"corresp-8","name":"Corresp 8","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":12,"slug":"corresp-9","name":"Corresp 9","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":17,"slug":"correspondent-14","name":"Correspondent 14","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":2,"slug":"correspondent-2","name":"Correspondent 2","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":7,"last_correspondence":"2021-01-20T23:37:58.204614Z"},{"id":27,"slug":"michael-shamoon","name":"Michael Shamoon","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2022-03-16T03:48:50.089624Z"},{"id":4,"slug":"newest-correspondent","name":"Newest Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2021-02-07T08:00:00Z"}]}
|
{
|
||||||
|
"count": 27,
|
||||||
|
"next": "http://localhost:8000/api/correspondents/?page=2",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"slug": "abc-test-correspondent",
|
||||||
|
"name": "ABC Test Correspondent",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"slug": "corresp-10",
|
||||||
|
"name": "Corresp 10",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"slug": "corresp-11",
|
||||||
|
"name": "Corresp 11",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"slug": "corresp-12",
|
||||||
|
"name": "Corresp 12",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"slug": "corresp-13",
|
||||||
|
"name": "Corresp 13",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"slug": "corresp-15",
|
||||||
|
"name": "Corresp 15",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"slug": "corresp-16",
|
||||||
|
"name": "Corresp 16",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"slug": "corresp-17",
|
||||||
|
"name": "Corresp 17",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"slug": "corresp-18",
|
||||||
|
"name": "Corresp 18",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"slug": "corresp-19",
|
||||||
|
"name": "Corresp 19",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"slug": "corresp-20",
|
||||||
|
"name": "Corresp 20",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"slug": "corresp-21",
|
||||||
|
"name": "Corresp 21",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"slug": "corresp-22",
|
||||||
|
"name": "Corresp 22",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"slug": "corresp-23",
|
||||||
|
"name": "Corresp 23",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"slug": "corresp-3",
|
||||||
|
"name": "Corresp 3",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"slug": "corresp-4",
|
||||||
|
"name": "Corresp 4",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"slug": "corresp-5",
|
||||||
|
"name": "Corresp 5",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"slug": "corresp-6",
|
||||||
|
"name": "Corresp 6",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"slug": "corresp-7",
|
||||||
|
"name": "Corresp 7",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"slug": "corresp-8",
|
||||||
|
"name": "Corresp 8",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"slug": "corresp-9",
|
||||||
|
"name": "Corresp 9",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"slug": "correspondent-14",
|
||||||
|
"name": "Correspondent 14",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"slug": "correspondent-2",
|
||||||
|
"name": "Correspondent 2",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 7,
|
||||||
|
"last_correspondence": "2021-01-20T23:37:58.204614Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"slug": "correspondent-slug",
|
||||||
|
"name": "Correspondent Slug",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1,
|
||||||
|
"last_correspondence": "2022-03-16T03:48:50.089624Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"slug": "newest-correspondent",
|
||||||
|
"name": "Newest Correspondent",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1,
|
||||||
|
"last_correspondence": "2021-02-07T08:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -1 +1,25 @@
|
|||||||
{"count":1,"next":null,"previous":null,"results":[{"id":1,"slug":"test","name":"Test Doc Type","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0}]}
|
{
|
||||||
|
"count": 2,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test",
|
||||||
|
"name": "Test Doc Type",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"slug": "test2",
|
||||||
|
"name": "Test Doc Type 2",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"comment": "Testing new comment",
|
|
||||||
"created": "2022-08-08T04:24:55.176008Z",
|
|
||||||
"user": {
|
|
||||||
"id": 1,
|
|
||||||
"username": "user2",
|
|
||||||
"first_name": "",
|
|
||||||
"last_name": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9,
|
|
||||||
"comment": "Testing one more time",
|
|
||||||
"created": "2022-02-18T04:24:55.176008Z",
|
|
||||||
"user": {
|
|
||||||
"id": 2,
|
|
||||||
"username": "user1",
|
|
||||||
"first_name": "",
|
|
||||||
"last_name": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 8,
|
|
||||||
"comment": "Another comment",
|
|
||||||
"created": "2021-11-08T04:24:47.925042Z",
|
|
||||||
"user": {
|
|
||||||
"id": 2,
|
|
||||||
"username": "user33",
|
|
||||||
"first_name": "",
|
|
||||||
"last_name": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 7,
|
|
||||||
"comment": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
|
||||||
"created": "2021-02-08T02:37:49.724132Z",
|
|
||||||
"user": {
|
|
||||||
"id": 3,
|
|
||||||
"username": "admin",
|
|
||||||
"first_name": "",
|
|
||||||
"last_name": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
26
src-ui/cypress/fixtures/documents/1/notes.json
Normal file
26
src-ui/cypress/fixtures/documents/1/notes.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"note": "Testing new note",
|
||||||
|
"created": "2022-08-08T04:24:55.176008Z",
|
||||||
|
"user": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"note": "Testing one more time",
|
||||||
|
"created": "2022-02-18T04:24:55.176008Z",
|
||||||
|
"user": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"note": "Another note",
|
||||||
|
"created": "2021-11-08T04:24:47.925042Z",
|
||||||
|
"user": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"note": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||||
|
"created": "2021-02-08T02:37:49.724132Z",
|
||||||
|
"user": 3
|
||||||
|
}
|
||||||
|
]
|
@ -21,7 +21,27 @@
|
|||||||
"original_file_name": "2022-03-22 no latin title.pdf",
|
"original_file_name": "2022-03-22 no latin title.pdf",
|
||||||
"archived_file_name": "2022-03-22 no latin title.pdf",
|
"archived_file_name": "2022-03-22 no latin title.pdf",
|
||||||
"owner": null,
|
"owner": null,
|
||||||
"permissions": []
|
"permissions": [],
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"note": "Testing one more time",
|
||||||
|
"created": "2022-02-18T04:24:55.176008Z",
|
||||||
|
"user": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"note": "Another note",
|
||||||
|
"created": "2021-11-08T04:24:47.925042Z",
|
||||||
|
"user": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"note": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||||
|
"created": "2021-02-08T02:37:49.724132Z",
|
||||||
|
"user": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
@ -39,11 +59,12 @@
|
|||||||
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
||||||
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf",
|
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf",
|
||||||
"owner": null,
|
"owner": null,
|
||||||
"permissions": []
|
"permissions": [],
|
||||||
|
"notes": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"correspondent": null,
|
"correspondent": 14,
|
||||||
"document_type": 1,
|
"document_type": 1,
|
||||||
"storage_path": null,
|
"storage_path": null,
|
||||||
"title": "dolor",
|
"title": "dolor",
|
||||||
@ -59,12 +80,13 @@
|
|||||||
"original_file_name": "2022-03-24 dolor.pdf",
|
"original_file_name": "2022-03-24 dolor.pdf",
|
||||||
"archived_file_name": "2022-03-24 dolor.pdf",
|
"archived_file_name": "2022-03-24 dolor.pdf",
|
||||||
"owner": null,
|
"owner": null,
|
||||||
"permissions": []
|
"permissions": [],
|
||||||
|
"notes": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"correspondent": 9,
|
"correspondent": 9,
|
||||||
"document_type": 1,
|
"document_type": 2,
|
||||||
"storage_path": null,
|
"storage_path": null,
|
||||||
"title": "sit amet",
|
"title": "sit amet",
|
||||||
"content": "Test document PDF",
|
"content": "Test document PDF",
|
||||||
@ -79,7 +101,8 @@
|
|||||||
"original_file_name": "2022-06-01 sit amet.pdf",
|
"original_file_name": "2022-06-01 sit amet.pdf",
|
||||||
"archived_file_name": "2022-06-01 sit amet.pdf",
|
"archived_file_name": "2022-06-01 sit amet.pdf",
|
||||||
"owner": null,
|
"owner": null,
|
||||||
"permissions": []
|
"permissions": [],
|
||||||
|
"notes": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@
|
|||||||
"change_user",
|
"change_user",
|
||||||
"delete_user",
|
"delete_user",
|
||||||
"view_user",
|
"view_user",
|
||||||
"add_comment",
|
"add_note",
|
||||||
"change_comment",
|
"change_note",
|
||||||
"delete_comment",
|
"delete_note",
|
||||||
"view_comment"
|
"view_note"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -73,10 +73,10 @@
|
|||||||
"change_task",
|
"change_task",
|
||||||
"delete_task",
|
"delete_task",
|
||||||
"view_task",
|
"view_task",
|
||||||
"add_comment",
|
"add_note",
|
||||||
"change_comment",
|
"change_note",
|
||||||
"delete_comment",
|
"delete_note",
|
||||||
"view_comment",
|
"view_note",
|
||||||
"add_correspondent",
|
"add_correspondent",
|
||||||
"change_correspondent",
|
"change_correspondent",
|
||||||
"delete_correspondent",
|
"delete_correspondent",
|
||||||
|
@ -94,10 +94,10 @@
|
|||||||
"change_task",
|
"change_task",
|
||||||
"delete_task",
|
"delete_task",
|
||||||
"view_task",
|
"view_task",
|
||||||
"add_comment",
|
"add_note",
|
||||||
"change_comment",
|
"change_note",
|
||||||
"delete_comment",
|
"delete_note",
|
||||||
"view_comment",
|
"view_note",
|
||||||
"add_correspondent",
|
"add_correspondent",
|
||||||
"change_correspondent",
|
"change_correspondent",
|
||||||
"delete_correspondent",
|
"delete_correspondent",
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
"change_task",
|
"change_task",
|
||||||
"delete_task",
|
"delete_task",
|
||||||
"view_task",
|
"view_task",
|
||||||
"add_comment",
|
"add_note",
|
||||||
"add_frontendsettings",
|
"add_frontendsettings",
|
||||||
"change_frontendsettings",
|
"change_frontendsettings",
|
||||||
"delete_frontendsettings",
|
"delete_frontendsettings",
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
"django_celery_results.delete_taskresult",
|
"django_celery_results.delete_taskresult",
|
||||||
"paperless_mail.add_mailaccount",
|
"paperless_mail.add_mailaccount",
|
||||||
"auth.change_group",
|
"auth.change_group",
|
||||||
"documents.add_comment",
|
"documents.add_note",
|
||||||
"paperless_mail.delete_mailaccount",
|
"paperless_mail.delete_mailaccount",
|
||||||
"authtoken.delete_tokenproxy",
|
"authtoken.delete_tokenproxy",
|
||||||
"guardian.delete_groupobjectpermission",
|
"guardian.delete_groupobjectpermission",
|
||||||
@ -44,7 +44,7 @@
|
|||||||
"documents.add_documenttype",
|
"documents.add_documenttype",
|
||||||
"django_q.change_success",
|
"django_q.change_success",
|
||||||
"documents.delete_tag",
|
"documents.delete_tag",
|
||||||
"documents.change_comment",
|
"documents.change_note",
|
||||||
"django_q.delete_task",
|
"django_q.delete_task",
|
||||||
"documents.add_savedviewfilterrule",
|
"documents.add_savedviewfilterrule",
|
||||||
"django_q.view_task",
|
"django_q.view_task",
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"documents.add_savedview",
|
"documents.add_savedview",
|
||||||
"auth.delete_user",
|
"auth.delete_user",
|
||||||
"documents.view_log",
|
"documents.view_log",
|
||||||
"documents.view_comment",
|
"documents.view_note",
|
||||||
"guardian.change_groupobjectpermission",
|
"guardian.change_groupobjectpermission",
|
||||||
"sessions.delete_session",
|
"sessions.delete_session",
|
||||||
"django_q.change_failure",
|
"django_q.change_failure",
|
||||||
@ -139,7 +139,7 @@
|
|||||||
"django_celery_results.view_taskresult",
|
"django_celery_results.view_taskresult",
|
||||||
"contenttypes.add_contenttype",
|
"contenttypes.add_contenttype",
|
||||||
"django_q.delete_success",
|
"django_q.delete_success",
|
||||||
"documents.delete_comment",
|
"documents.delete_note",
|
||||||
"django_q.add_failure",
|
"django_q.add_failure",
|
||||||
"guardian.add_userobjectpermission",
|
"guardian.add_userobjectpermission",
|
||||||
"sessions.view_session",
|
"sessions.view_session",
|
||||||
@ -216,10 +216,10 @@
|
|||||||
"change_task",
|
"change_task",
|
||||||
"delete_task",
|
"delete_task",
|
||||||
"view_task",
|
"view_task",
|
||||||
"add_comment",
|
"add_note",
|
||||||
"change_comment",
|
"change_note",
|
||||||
"delete_comment",
|
"delete_note",
|
||||||
"view_comment",
|
"view_note",
|
||||||
"add_frontendsettings",
|
"add_frontendsettings",
|
||||||
"change_frontendsettings",
|
"change_frontendsettings",
|
||||||
"delete_frontendsettings",
|
"delete_frontendsettings",
|
||||||
@ -256,7 +256,7 @@
|
|||||||
"django_celery_results.delete_taskresult",
|
"django_celery_results.delete_taskresult",
|
||||||
"authtoken.change_token",
|
"authtoken.change_token",
|
||||||
"auth.change_group",
|
"auth.change_group",
|
||||||
"documents.add_comment",
|
"documents.add_note",
|
||||||
"authtoken.delete_tokenproxy",
|
"authtoken.delete_tokenproxy",
|
||||||
"documents.view_documenttype",
|
"documents.view_documenttype",
|
||||||
"contenttypes.delete_contenttype",
|
"contenttypes.delete_contenttype",
|
||||||
@ -285,7 +285,7 @@
|
|||||||
"django_q.change_task",
|
"django_q.change_task",
|
||||||
"sessions.add_session",
|
"sessions.add_session",
|
||||||
"documents.change_taskattributes",
|
"documents.change_taskattributes",
|
||||||
"documents.change_comment",
|
"documents.change_note",
|
||||||
"django_q.delete_task",
|
"django_q.delete_task",
|
||||||
"django_q.delete_ormq",
|
"django_q.delete_ormq",
|
||||||
"auth.change_permission",
|
"auth.change_permission",
|
||||||
@ -311,7 +311,7 @@
|
|||||||
"documents.view_document",
|
"documents.view_document",
|
||||||
"documents.add_savedview",
|
"documents.add_savedview",
|
||||||
"django_q.view_failure",
|
"django_q.view_failure",
|
||||||
"documents.view_comment",
|
"documents.view_note",
|
||||||
"documents.view_log",
|
"documents.view_log",
|
||||||
"documents.add_log",
|
"documents.add_log",
|
||||||
"documents.change_savedview",
|
"documents.change_savedview",
|
||||||
@ -324,7 +324,7 @@
|
|||||||
"django_celery_results.view_taskresult",
|
"django_celery_results.view_taskresult",
|
||||||
"contenttypes.add_contenttype",
|
"contenttypes.add_contenttype",
|
||||||
"django_q.delete_success",
|
"django_q.delete_success",
|
||||||
"documents.delete_comment",
|
"documents.delete_note",
|
||||||
"django_q.add_failure",
|
"django_q.add_failure",
|
||||||
"sessions.view_session",
|
"sessions.view_session",
|
||||||
"contenttypes.view_contenttype",
|
"contenttypes.view_contenttype",
|
||||||
@ -373,7 +373,7 @@
|
|||||||
"django_celery_results.delete_taskresult",
|
"django_celery_results.delete_taskresult",
|
||||||
"authtoken.change_token",
|
"authtoken.change_token",
|
||||||
"auth.change_group",
|
"auth.change_group",
|
||||||
"documents.add_comment",
|
"documents.add_note",
|
||||||
"authtoken.delete_tokenproxy",
|
"authtoken.delete_tokenproxy",
|
||||||
"documents.view_documenttype",
|
"documents.view_documenttype",
|
||||||
"contenttypes.delete_contenttype",
|
"contenttypes.delete_contenttype",
|
||||||
@ -402,7 +402,7 @@
|
|||||||
"django_q.change_task",
|
"django_q.change_task",
|
||||||
"sessions.add_session",
|
"sessions.add_session",
|
||||||
"documents.change_taskattributes",
|
"documents.change_taskattributes",
|
||||||
"documents.change_comment",
|
"documents.change_note",
|
||||||
"django_q.delete_task",
|
"django_q.delete_task",
|
||||||
"django_q.delete_ormq",
|
"django_q.delete_ormq",
|
||||||
"auth.change_permission",
|
"auth.change_permission",
|
||||||
@ -429,7 +429,7 @@
|
|||||||
"documents.view_document",
|
"documents.view_document",
|
||||||
"documents.add_savedview",
|
"documents.add_savedview",
|
||||||
"django_q.view_failure",
|
"django_q.view_failure",
|
||||||
"documents.view_comment",
|
"documents.view_note",
|
||||||
"documents.view_log",
|
"documents.view_log",
|
||||||
"auth.delete_user",
|
"auth.delete_user",
|
||||||
"documents.add_log",
|
"documents.add_log",
|
||||||
@ -443,7 +443,7 @@
|
|||||||
"django_celery_results.view_taskresult",
|
"django_celery_results.view_taskresult",
|
||||||
"contenttypes.add_contenttype",
|
"contenttypes.add_contenttype",
|
||||||
"django_q.delete_success",
|
"django_q.delete_success",
|
||||||
"documents.delete_comment",
|
"documents.delete_note",
|
||||||
"django_q.add_failure",
|
"django_q.add_failure",
|
||||||
"sessions.view_session",
|
"sessions.view_session",
|
||||||
"contenttypes.view_contenttype",
|
"contenttypes.view_contenttype",
|
||||||
|
File diff suppressed because it is too large
Load Diff
6
src-ui/package-lock.json
generated
6
src-ui/package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"mime-names": "^1.0.0",
|
||||||
"ng2-pdf-viewer": "^9.1.2",
|
"ng2-pdf-viewer": "^9.1.2",
|
||||||
"ngx-color": "^8.0.3",
|
"ngx-color": "^8.0.3",
|
||||||
"ngx-cookie-service": "^15.0.0",
|
"ngx-cookie-service": "^15.0.0",
|
||||||
@ -13766,6 +13767,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-names": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-names/-/mime-names-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-vLNEfYU63fz34panv/L3Lh3eW3+v0BlOB+bSGFdntv/gBNnokCbSsaNuHR9vH/NS5oWbL0HqMQf/3we4fRJyIQ=="
|
||||||
|
},
|
||||||
"node_modules/mime-types": {
|
"node_modules/mime-types": {
|
||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"mime-names": "^1.0.0",
|
||||||
"ng2-pdf-viewer": "^9.1.2",
|
"ng2-pdf-viewer": "^9.1.2",
|
||||||
"ngx-color": "^8.0.3",
|
"ngx-color": "^8.0.3",
|
||||||
"ngx-cookie-service": "^15.0.0",
|
"ngx-cookie-service": "^15.0.0",
|
||||||
|
@ -65,6 +65,17 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'documents/:id/:section',
|
||||||
|
component: DocumentDetailComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'asn/:id',
|
path: 'asn/:id',
|
||||||
component: DocumentAsnComponent,
|
component: DocumentAsnComponent,
|
||||||
@ -143,17 +154,6 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'tasks',
|
|
||||||
component: TasksComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.PaperlessTask,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'settings/:section',
|
path: 'settings/:section',
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
@ -171,6 +171,17 @@ const routes: Routes = [
|
|||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
canDeactivate: [DirtyFormGuard],
|
canDeactivate: [DirtyFormGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tasks',
|
||||||
|
component: TasksComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.PaperlessTask,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ path: 'tasks', component: TasksComponent },
|
{ path: 'tasks', component: TasksComponent },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -70,7 +70,7 @@ import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
|
|||||||
import { ColorSliderModule } from 'ngx-color/slider'
|
import { ColorSliderModule } from 'ngx-color/slider'
|
||||||
import { ColorComponent } from './components/common/input/color/color.component'
|
import { ColorComponent } from './components/common/input/color/color.component'
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
|
||||||
import { PermissionsGuard } from './guards/permissions.guard'
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
@ -196,7 +196,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DateComponent,
|
DateComponent,
|
||||||
ColorComponent,
|
ColorComponent,
|
||||||
DocumentAsnComponent,
|
DocumentAsnComponent,
|
||||||
DocumentCommentsComponent,
|
DocumentNotesComponent,
|
||||||
TasksComponent,
|
TasksComponent,
|
||||||
UserEditDialogComponent,
|
UserEditDialogComponent,
|
||||||
GroupEditDialogComponent,
|
GroupEditDialogComponent,
|
||||||
|
@ -20,7 +20,7 @@ export abstract class EditDialogComponent<
|
|||||||
> implements OnInit
|
> implements OnInit
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
protected service: AbstractPaperlessService<T>,
|
||||||
private activeModal: NgbActiveModal,
|
private activeModal: NgbActiveModal,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
@ -15,11 +15,22 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||||
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||||
|
<app-input-check i18n-title title="Password is token" i18n-hint hint="Check if the password above is a token used for authentication" formControlName="is_token" [error]="error?.is_token"></app-input-check>
|
||||||
<app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
|
<app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
<div class="m-0 me-2">
|
||||||
|
<ngb-alert #testResultAlert *ngIf="testResult" [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive">
|
||||||
|
<ng-container *ngIf="testActive">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span class="visually-hidden mr-1" i18n>Loading...</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container i18n>Test</ng-container>
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
::ng-deep .alert-dismissible .btn-close {
|
||||||
|
padding-top: 0.75rem !important;
|
||||||
|
padding-bottom: 0.75rem !important;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, ViewChild } from '@angular/core'
|
||||||
import { FormControl, FormGroup } from '@angular/forms'
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
import {
|
import {
|
||||||
IMAPSecurity,
|
IMAPSecurity,
|
||||||
@ -21,6 +21,12 @@ const IMAP_SECURITY_OPTIONS = [
|
|||||||
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||||
|
testActive: boolean = false
|
||||||
|
testResult: string
|
||||||
|
alertTimeout
|
||||||
|
|
||||||
|
@ViewChild('testResultAlert', { static: false }) testResultAlert: NgbAlert
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
service: MailAccountService,
|
service: MailAccountService,
|
||||||
activeModal: NgbActiveModal,
|
activeModal: NgbActiveModal,
|
||||||
@ -45,6 +51,7 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
|
|||||||
imap_security: new FormControl(IMAPSecurity.SSL),
|
imap_security: new FormControl(IMAPSecurity.SSL),
|
||||||
username: new FormControl(null),
|
username: new FormControl(null),
|
||||||
password: new FormControl(null),
|
password: new FormControl(null),
|
||||||
|
is_token: new FormControl(false),
|
||||||
character_set: new FormControl('UTF-8'),
|
character_set: new FormControl('UTF-8'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -52,4 +59,33 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
|
|||||||
get imapSecurityOptions() {
|
get imapSecurityOptions() {
|
||||||
return IMAP_SECURITY_OPTIONS
|
return IMAP_SECURITY_OPTIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test() {
|
||||||
|
this.testActive = true
|
||||||
|
this.testResult = null
|
||||||
|
clearTimeout(this.alertTimeout)
|
||||||
|
const mailService = this.service as MailAccountService
|
||||||
|
const newObject = Object.assign(
|
||||||
|
Object.assign({}, this.object),
|
||||||
|
this.objectForm.value
|
||||||
|
)
|
||||||
|
mailService.test(newObject).subscribe({
|
||||||
|
next: (result: { success: boolean }) => {
|
||||||
|
this.testActive = false
|
||||||
|
this.testResult = result.success ? 'success' : 'danger'
|
||||||
|
this.alertTimeout = setTimeout(() => this.testResultAlert.close(), 5000)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.testActive = false
|
||||||
|
this.testResult = 'danger'
|
||||||
|
this.alertTimeout = setTimeout(() => this.testResultAlert.close(), 5000)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get testResultMessage() {
|
||||||
|
return this.testResult === 'success'
|
||||||
|
? $localize`Successfully connected to the mail server`
|
||||||
|
: $localize`Unable to connect to the mail server`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,29 @@
|
|||||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||||
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
||||||
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
<app-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<div *ngIf="!editing && multiple" class="list-group-item d-flex">
|
<div *ngIf="!editing && manyToOne" class="list-group-item d-flex">
|
||||||
<div class="btn-group btn-group-xs flex-fill">
|
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and">
|
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
|
||||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label>
|
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
|
||||||
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or">
|
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or">
|
||||||
<label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label>
|
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!editing && !manyToOne" class="list-group-item d-flex">
|
||||||
|
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||||
|
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
|
||||||
|
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
|
||||||
|
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude">
|
||||||
|
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
@ -34,7 +42,7 @@
|
|||||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="!editing && multiple" class="list-group-item list-group-item-note pt-1 pb-2">
|
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||||
<small i18n>Click again to exclude items.</small>
|
<small i18n>Click again to exclude items.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,12 +18,25 @@ export interface ChangedItems {
|
|||||||
itemsToRemove: MatchingModel[]
|
itemsToRemove: MatchingModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LogicalOperator {
|
||||||
|
And = 'and',
|
||||||
|
Or = 'or',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Intersection {
|
||||||
|
Include = 'include',
|
||||||
|
Exclude = 'exclude',
|
||||||
|
}
|
||||||
|
|
||||||
export class FilterableDropdownSelectionModel {
|
export class FilterableDropdownSelectionModel {
|
||||||
changed = new Subject<FilterableDropdownSelectionModel>()
|
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
multiple = false
|
manyToOne = false
|
||||||
private _logicalOperator = 'and'
|
singleSelect = false
|
||||||
temporaryLogicalOperator = this._logicalOperator
|
private _logicalOperator: LogicalOperator = LogicalOperator.And
|
||||||
|
temporaryLogicalOperator: LogicalOperator = this._logicalOperator
|
||||||
|
private _intersection: Intersection = Intersection.Include
|
||||||
|
temporaryIntersection: Intersection = this._intersection
|
||||||
|
|
||||||
items: MatchingModel[] = []
|
items: MatchingModel[] = []
|
||||||
|
|
||||||
@ -86,7 +99,30 @@ export class FilterableDropdownSelectionModel {
|
|||||||
(state != ToggleableItemState.Selected &&
|
(state != ToggleableItemState.Selected &&
|
||||||
state != ToggleableItemState.Excluded)
|
state != ToggleableItemState.Excluded)
|
||||||
) {
|
) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
if (this.manyToOne || this.singleSelect) {
|
||||||
|
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||||
|
|
||||||
|
if (this.singleSelect) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key != id) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let newState =
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
? ToggleableItemState.Selected
|
||||||
|
: ToggleableItemState.Excluded
|
||||||
|
if (!id) newState = ToggleableItemState.Selected
|
||||||
|
if (
|
||||||
|
state == ToggleableItemState.Excluded &&
|
||||||
|
this.intersection == Intersection.Exclude
|
||||||
|
) {
|
||||||
|
newState = ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
this.temporarySelectionStates.set(id, newState)
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
state == ToggleableItemState.Selected ||
|
state == ToggleableItemState.Selected ||
|
||||||
state == ToggleableItemState.Excluded
|
state == ToggleableItemState.Excluded
|
||||||
@ -94,14 +130,6 @@ export class FilterableDropdownSelectionModel {
|
|||||||
this.temporarySelectionStates.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.multiple) {
|
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
|
||||||
if (key != id) {
|
|
||||||
this.temporarySelectionStates.delete(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
if (key) {
|
if (key) {
|
||||||
@ -119,19 +147,36 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
exclude(id: number, fireEvent: boolean = true) {
|
exclude(id: number, fireEvent: boolean = true) {
|
||||||
let state = this.temporarySelectionStates.get(id)
|
let state = this.temporarySelectionStates.get(id)
|
||||||
if (state == null || state != ToggleableItemState.Excluded) {
|
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
|
||||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
? LogicalOperator.And
|
||||||
} else if (state == ToggleableItemState.Excluded) {
|
: LogicalOperator.Or
|
||||||
this.temporarySelectionStates.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.multiple) {
|
if (this.manyToOne || this.singleSelect) {
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||||
if (key != id) {
|
|
||||||
this.temporarySelectionStates.delete(key)
|
if (this.singleSelect) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key != id) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let newState =
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
? ToggleableItemState.Selected
|
||||||
|
: ToggleableItemState.Excluded
|
||||||
|
if (
|
||||||
|
state == ToggleableItemState.Selected &&
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
) {
|
||||||
|
newState = ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
this.temporarySelectionStates.set(id, newState)
|
||||||
}
|
}
|
||||||
|
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||||
|
this.temporarySelectionStates.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
@ -143,11 +188,11 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
get logicalOperator(): string {
|
get logicalOperator(): LogicalOperator {
|
||||||
return this.temporaryLogicalOperator
|
return this.temporaryLogicalOperator
|
||||||
}
|
}
|
||||||
|
|
||||||
set logicalOperator(operator: string) {
|
set logicalOperator(operator: LogicalOperator) {
|
||||||
this.temporaryLogicalOperator = operator
|
this.temporaryLogicalOperator = operator
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +200,26 @@ export class FilterableDropdownSelectionModel {
|
|||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get intersection(): Intersection {
|
||||||
|
return this.temporaryIntersection
|
||||||
|
}
|
||||||
|
|
||||||
|
set intersection(intersection: Intersection) {
|
||||||
|
this.temporaryIntersection = intersection
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleIntersection() {
|
||||||
|
if (this.temporarySelectionStates.size === 0) return
|
||||||
|
let newState =
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
? ToggleableItemState.Selected
|
||||||
|
: ToggleableItemState.Excluded
|
||||||
|
this.temporarySelectionStates.forEach((state, key) => {
|
||||||
|
this.temporarySelectionStates.set(key, newState)
|
||||||
|
})
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
|
||||||
get(id: number) {
|
get(id: number) {
|
||||||
return (
|
return (
|
||||||
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
@ -171,7 +236,8 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
clear(fireEvent = true) {
|
clear(fireEvent = true) {
|
||||||
this.temporarySelectionStates.clear()
|
this.temporarySelectionStates.clear()
|
||||||
this.temporaryLogicalOperator = this._logicalOperator = 'and'
|
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||||
|
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
@ -194,6 +260,8 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return true
|
return true
|
||||||
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
||||||
return true
|
return true
|
||||||
|
} else if (this.temporaryIntersection !== this._intersection) {
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -217,13 +285,18 @@ export class FilterableDropdownSelectionModel {
|
|||||||
this.selectionStates.set(key, value)
|
this.selectionStates.set(key, value)
|
||||||
})
|
})
|
||||||
this._logicalOperator = this.temporaryLogicalOperator
|
this._logicalOperator = this.temporaryLogicalOperator
|
||||||
|
this._intersection = this.temporaryIntersection
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset(complete: boolean = false) {
|
||||||
this.temporarySelectionStates.clear()
|
this.temporarySelectionStates.clear()
|
||||||
this.selectionStates.forEach((value, key) => {
|
if (complete) {
|
||||||
this.temporarySelectionStates.set(key, value)
|
this.selectionStates.clear()
|
||||||
})
|
} else {
|
||||||
|
this.selectionStates.forEach((value, key) => {
|
||||||
|
this.temporarySelectionStates.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diff(): ChangedItems {
|
diff(): ChangedItems {
|
||||||
@ -269,14 +342,16 @@ export class FilterableDropdownComponent {
|
|||||||
return this._selectionModel.items
|
return this._selectionModel.items
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectionModel = new FilterableDropdownSelectionModel()
|
_selectionModel: FilterableDropdownSelectionModel =
|
||||||
|
new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||||
if (this.selectionModel) {
|
if (this.selectionModel) {
|
||||||
this.selectionModel.changed.complete()
|
this.selectionModel.changed.complete()
|
||||||
model.items = this.selectionModel.items
|
model.items = this.selectionModel.items
|
||||||
model.multiple = this.selectionModel.multiple
|
model.manyToOne = this.selectionModel.manyToOne
|
||||||
|
model.singleSelect = this.editing && !this.selectionModel.manyToOne
|
||||||
}
|
}
|
||||||
model.changed.subscribe((updatedModel) => {
|
model.changed.subscribe((updatedModel) => {
|
||||||
this.selectionModelChange.next(updatedModel)
|
this.selectionModelChange.next(updatedModel)
|
||||||
@ -292,12 +367,12 @@ export class FilterableDropdownComponent {
|
|||||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set multiple(value: boolean) {
|
set manyToOne(manyToOne: boolean) {
|
||||||
this.selectionModel.multiple = value
|
this.selectionModel.manyToOne = manyToOne
|
||||||
}
|
}
|
||||||
|
|
||||||
get multiple() {
|
get manyToOne() {
|
||||||
return this.selectionModel.multiple
|
return this.selectionModel.manyToOne
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@ -327,16 +402,20 @@ export class FilterableDropdownComponent {
|
|||||||
@Output()
|
@Output()
|
||||||
opened = new EventEmitter()
|
opened = new EventEmitter()
|
||||||
|
|
||||||
get operatorToggleEnabled(): boolean {
|
get modifierToggleEnabled(): boolean {
|
||||||
return (
|
return this.manyToOne
|
||||||
this.selectionModel.selectionSize() > 1 &&
|
? this.selectionModel.selectionSize() > 1 &&
|
||||||
this.selectionModel.getExcludedItems().length == 0
|
this.selectionModel.getExcludedItems().length == 0
|
||||||
)
|
: !this.selectionModel.isNoneSelected()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
documentCounts: SelectionDataItem[]
|
documentCounts: SelectionDataItem[]
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||||
|
}
|
||||||
|
|
||||||
getUpdatedDocumentCount(id: number) {
|
getUpdatedDocumentCount(id: number) {
|
||||||
if (this.documentCounts) {
|
if (this.documentCounts) {
|
||||||
return this.documentCounts.find((c) => c.id === id)?.document_count
|
return this.documentCounts.find((c) => c.id === id)?.document_count
|
||||||
@ -346,7 +425,6 @@ export class FilterableDropdownComponent {
|
|||||||
modelIsDirty: boolean = false
|
modelIsDirty: boolean = false
|
||||||
|
|
||||||
constructor(private filterPipe: FilterPipe) {
|
constructor(private filterPipe: FilterPipe) {
|
||||||
this.selectionModel = new FilterableDropdownSelectionModel()
|
|
||||||
this.selectionModelChange.subscribe((updatedModel) => {
|
this.selectionModelChange.subscribe((updatedModel) => {
|
||||||
this.modelIsDirty = updatedModel.isDirty()
|
this.modelIsDirty = updatedModel.isDirty()
|
||||||
})
|
})
|
||||||
@ -400,7 +478,7 @@ export class FilterableDropdownComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.selectionModel.reset()
|
this.selectionModel.reset(true)
|
||||||
this.selectionModelChange.emit(this.selectionModel)
|
this.selectionModelChange.emit(this.selectionModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[style.color]="textColor"
|
[style.color]="textColor"
|
||||||
[style.background]="backgroundColor"
|
[style.background]="backgroundColor"
|
||||||
|
[class.private]="isPrivate"
|
||||||
[clearable]="allowNull"
|
[clearable]="allowNull"
|
||||||
[items]="items"
|
[items]="items"
|
||||||
[addTag]="allowCreateNew && addItemRef"
|
[addTag]="allowCreateNew && addItemRef"
|
||||||
|
@ -12,3 +12,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .private .ng-value-container {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
@ -26,8 +26,23 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
|||||||
this.addItemRef = this.addItem.bind(this)
|
this.addItemRef = this.addItem.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_items: any[]
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
items: any[]
|
set items(items) {
|
||||||
|
if (this.value && items.find((i) => i.id === this.value) === undefined) {
|
||||||
|
items.push({
|
||||||
|
id: this.value,
|
||||||
|
name: $localize`Private`,
|
||||||
|
private: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this._items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
get items(): any[] {
|
||||||
|
return this._items
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
textColor: any
|
textColor: any
|
||||||
@ -61,6 +76,10 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
|||||||
return this.createNew.observers.length > 0
|
return this.createNew.observers.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPrivate(): boolean {
|
||||||
|
return this.items?.find((i) => i.id === this.value)?.private
|
||||||
|
}
|
||||||
|
|
||||||
getSuggestions() {
|
getSuggestions() {
|
||||||
if (this.suggestions && this.items) {
|
if (this.suggestions && this.items) {
|
||||||
return this.suggestions
|
return this.suggestions
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.h2 {
|
||||||
|
min-height: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.h2 {
|
||||||
|
min-height: 2.8rem;
|
||||||
|
}
|
||||||
|
}
|
@ -59,7 +59,7 @@ export class PermissionsSelectComponent
|
|||||||
this.updateDisabledStates()
|
this.updateDisabledStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
inheritedWarning: string = $localize`Inerhited from group`
|
inheritedWarning: string = $localize`Inherited from group`
|
||||||
|
|
||||||
constructor(private readonly permissionsService: PermissionsService) {
|
constructor(private readonly permissionsService: PermissionsService) {
|
||||||
for (const type in PermissionType) {
|
for (const type in PermissionType) {
|
||||||
|
@ -1,2 +1,9 @@
|
|||||||
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
|
<ng-container *ngIf="tag !== undefined; else privateTag" >
|
||||||
<a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
|
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
|
||||||
|
<a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #privateTag>
|
||||||
|
<span *ngIf="!clickable" class="badge private" i18n>Private</span>
|
||||||
|
<a [title]="linkTitle" *ngIf="clickable" class="badge private" i18n>Private</a>
|
||||||
|
</ng-template>
|
||||||
|
@ -4,3 +4,10 @@ a {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.private {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
opacity: .5;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,46 @@
|
|||||||
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<p class="card-text" i18n *ngIf="statistics?.documents_inbox !== null">Documents in inbox: {{statistics?.documents_inbox}}</p>
|
<div class="list-group border-light">
|
||||||
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
|
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void()" *ngIf="statistics?.documents_inbox !== null" (click)="goToInbox()">
|
||||||
|
<ng-container i18n>Documents in inbox</ng-container>:
|
||||||
|
<span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span>
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/">
|
||||||
|
<ng-container i18n>Total documents</ng-container>:
|
||||||
|
<span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span>
|
||||||
|
</a>
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
|
||||||
|
<ng-container i18n>Total characters</ng-container>:
|
||||||
|
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="statistics?.document_file_type_counts?.length > 1" class="list-group-item filetypes">
|
||||||
|
<div class="d-flex justify-content-between align-items-center my-2">
|
||||||
|
<div class="progress flex-grow-1">
|
||||||
|
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last"
|
||||||
|
class="progress-bar bg-primary text-primary-contrast"
|
||||||
|
role="progressbar"
|
||||||
|
[ngbPopover]="getFileTypeName(filetype)"
|
||||||
|
i18n-ngbPopover
|
||||||
|
triggers="mouseenter:mouseleave"
|
||||||
|
[attr.aria-label]="getFileTypeName(filetype)"
|
||||||
|
[class.me-1px]="!last"
|
||||||
|
[style.width]="getFileTypePercent(filetype) + '%'"
|
||||||
|
[style.opacity]="getItemOpacity(i)"
|
||||||
|
[attr.aria-valuenow]="getFileTypePercent(filetype)"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap align-items-start">
|
||||||
|
<div class="d-flex" *ngFor="let filetype of statistics?.document_file_type_counts; let i = index">
|
||||||
|
<div class="text-nowrap me-2">
|
||||||
|
<span class="badge rounded-pill bg-primary d-inline-block p-0 me-1" [style.opacity]="getItemOpacity(i)"></span>
|
||||||
|
<small class="text-nowrap"><span class="fw-bold">{{ getFileTypeExtension(filetype) }}</span> <span class="text-muted">({{getFileTypePercent(filetype) | number: '1.0-1'}}%)</span></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-widget-frame>
|
</app-widget-frame>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
.filetypes {
|
||||||
|
.progress {
|
||||||
|
height: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
height: 0.6rem;
|
||||||
|
width: 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,23 @@
|
|||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { Observable, Subscription } from 'rxjs'
|
import { Observable, Subscription } from 'rxjs'
|
||||||
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
|
import * as mimeTypeNames from 'mime-names'
|
||||||
|
|
||||||
export interface Statistics {
|
export interface Statistics {
|
||||||
documents_total?: number
|
documents_total?: number
|
||||||
documents_inbox?: number
|
documents_inbox?: number
|
||||||
|
inbox_tag?: number
|
||||||
|
document_file_type_counts?: DocumentFileType[]
|
||||||
|
character_count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentFileType {
|
||||||
|
mime_type: string
|
||||||
|
mime_type_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -19,7 +30,8 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private consumerStatusService: ConsumerStatusService
|
private consumerStatusService: ConsumerStatusService,
|
||||||
|
private documentListViewService: DocumentListViewService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
statistics: Statistics = {}
|
statistics: Statistics = {}
|
||||||
@ -34,10 +46,43 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
this.loading = true
|
this.loading = true
|
||||||
this.getStatistics().subscribe((statistics) => {
|
this.getStatistics().subscribe((statistics) => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
const fileTypeMax = 5
|
||||||
|
if (statistics.document_file_type_counts?.length > fileTypeMax) {
|
||||||
|
const others = statistics.document_file_type_counts.slice(fileTypeMax)
|
||||||
|
statistics.document_file_type_counts =
|
||||||
|
statistics.document_file_type_counts.slice(0, fileTypeMax)
|
||||||
|
statistics.document_file_type_counts.push({
|
||||||
|
mime_type: $localize`Other`,
|
||||||
|
mime_type_count: others.reduce(
|
||||||
|
(currentValue, documentFileType) =>
|
||||||
|
documentFileType.mime_type_count + currentValue,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
this.statistics = statistics
|
this.statistics = statistics
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFileTypeExtension(filetype: DocumentFileType): string {
|
||||||
|
return (
|
||||||
|
mimeTypeNames[filetype.mime_type]?.extensions[0]?.toUpperCase() ??
|
||||||
|
filetype.mime_type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileTypeName(filetype: DocumentFileType): string {
|
||||||
|
return mimeTypeNames[filetype.mime_type]?.name ?? filetype.mime_type
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileTypePercent(filetype: DocumentFileType): number {
|
||||||
|
return (filetype.mime_type_count / this.statistics?.documents_total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemOpacity(i: number): number {
|
||||||
|
return 1 - i / this.statistics?.document_file_type_counts.length
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.reload()
|
this.reload()
|
||||||
this.subscription = this.consumerStatusService
|
this.subscription = this.consumerStatusService
|
||||||
@ -50,4 +95,13 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscription.unsubscribe()
|
this.subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToInbox() {
|
||||||
|
this.documentListViewService.quickFilter([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_TAGS_ALL,
|
||||||
|
value: this.statistics.inbox_tag.toString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
|
||||||
import { DocumentCommentsService } from 'src/app/services/rest/document-comments.service'
|
|
||||||
import { PaperlessDocumentComment } from 'src/app/data/paperless-document-comment'
|
|
||||||
import { FormControl, FormGroup } from '@angular/forms'
|
|
||||||
import { first } from 'rxjs/operators'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-document-comments',
|
|
||||||
templateUrl: './document-comments.component.html',
|
|
||||||
styleUrls: ['./document-comments.component.scss'],
|
|
||||||
})
|
|
||||||
export class DocumentCommentsComponent extends ComponentWithPermissions {
|
|
||||||
commentForm: FormGroup = new FormGroup({
|
|
||||||
newComment: new FormControl(''),
|
|
||||||
})
|
|
||||||
|
|
||||||
networkActive = false
|
|
||||||
comments: PaperlessDocumentComment[] = []
|
|
||||||
newCommentError: boolean = false
|
|
||||||
|
|
||||||
private _documentId: number
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
set documentId(id: number) {
|
|
||||||
if (id != this._documentId) {
|
|
||||||
this._documentId = id
|
|
||||||
this.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private commentsService: DocumentCommentsService,
|
|
||||||
private toastService: ToastService
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
update(): void {
|
|
||||||
this.networkActive = true
|
|
||||||
this.commentsService
|
|
||||||
.getComments(this._documentId)
|
|
||||||
.pipe(first())
|
|
||||||
.subscribe((comments) => {
|
|
||||||
this.comments = comments
|
|
||||||
this.networkActive = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addComment() {
|
|
||||||
const comment: string = this.commentForm
|
|
||||||
.get('newComment')
|
|
||||||
.value.toString()
|
|
||||||
.trim()
|
|
||||||
if (comment.length == 0) {
|
|
||||||
this.newCommentError = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.newCommentError = false
|
|
||||||
this.networkActive = true
|
|
||||||
this.commentsService.addComment(this._documentId, comment).subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
this.comments = result
|
|
||||||
this.commentForm.get('newComment').reset()
|
|
||||||
this.networkActive = false
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.networkActive = false
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`Error saving comment: ${e.toString()}`
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteComment(commentId: number) {
|
|
||||||
this.commentsService.deleteComment(this._documentId, commentId).subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
this.comments = result
|
|
||||||
this.networkActive = false
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.networkActive = false
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`Error deleting comment: ${e.toString()}`
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
displayName(comment: PaperlessDocumentComment): string {
|
|
||||||
if (!comment.user) return ''
|
|
||||||
let nameComponents = []
|
|
||||||
if (comment.user.first_name) nameComponents.unshift(comment.user.first_name)
|
|
||||||
if (comment.user.last_name) nameComponents.unshift(comment.user.last_name)
|
|
||||||
if (comment.user.username) {
|
|
||||||
if (nameComponents.length > 0)
|
|
||||||
nameComponents.push(`(${comment.user.username})`)
|
|
||||||
else nameComponents.push(comment.user.username)
|
|
||||||
}
|
|
||||||
return nameComponents.join(' ')
|
|
||||||
}
|
|
||||||
}
|
|
@ -67,8 +67,8 @@
|
|||||||
|
|
||||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" class="nav-tabs" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
|
||||||
<li [ngbNavItem]="1">
|
<li [ngbNavItem]="DocumentDetailNavIDs.Details">
|
||||||
<a ngbNavLink i18n>Details</a>
|
<a ngbNavLink i18n>Details</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
@ -87,7 +87,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="2">
|
<li [ngbNavItem]="DocumentDetailNavIDs.Content">
|
||||||
<a ngbNavLink i18n>Content</a>
|
<a ngbNavLink i18n>Content</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -96,7 +96,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="3">
|
<li [ngbNavItem]="DocumentDetailNavIDs.Metadata">
|
||||||
<a ngbNavLink i18n>Metadata</a>
|
<a ngbNavLink i18n>Metadata</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
@ -147,7 +147,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="4" class="d-md-none">
|
<li [ngbNavItem]="DocumentDetailNavIDs.Preview" class="d-md-none">
|
||||||
<a ngbNavLink i18n>Preview</a>
|
<a ngbNavLink i18n>Preview</a>
|
||||||
<ng-template ngbNavContent *ngIf="!pdfPreview.offsetParent">
|
<ng-template ngbNavContent *ngIf="!pdfPreview.offsetParent">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
@ -171,14 +171,14 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="5" *ngIf="commentsEnabled">
|
<li [ngbNavItem]="DocumentDetailNavIDs.Notes" *ngIf="notesEnabled">
|
||||||
<a ngbNavLink i18n>Comments</a>
|
<a ngbNavLink i18n>Notes <span *ngIf="document?.notes.length" class="badge text-bg-secondary ms-1">{{document.notes.length}}</span></a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<app-document-comments [documentId]="documentId"></app-document-comments>
|
<app-document-notes [documentId]="documentId" [notes]="document?.notes" (updated)="notesUpdated($event)"></app-document-notes>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="6" *appIfOwner="document">
|
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions" *appIfOwner="document">
|
||||||
<a ngbNavLink i18n>Permissions</a>
|
<a ngbNavLink i18n>Permissions</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'
|
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'
|
||||||
import { FormControl, FormGroup } from '@angular/forms'
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { NgbModal, NgbNav } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbNav, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'
|
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'
|
||||||
@ -42,6 +42,16 @@ import {
|
|||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
|
||||||
|
|
||||||
|
enum DocumentDetailNavIDs {
|
||||||
|
Details = 1,
|
||||||
|
Content = 2,
|
||||||
|
Metadata = 3,
|
||||||
|
Preview = 4,
|
||||||
|
Notes = 5,
|
||||||
|
Permissions = 6,
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-detail',
|
selector: 'app-document-detail',
|
||||||
@ -117,6 +127,8 @@ export class DocumentDetailComponent
|
|||||||
|
|
||||||
PermissionAction = PermissionAction
|
PermissionAction = PermissionAction
|
||||||
PermissionType = PermissionType
|
PermissionType = PermissionType
|
||||||
|
DocumentDetailNavIDs = DocumentDetailNavIDs
|
||||||
|
activeNavID: number
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentsService: DocumentService,
|
private documentsService: DocumentService,
|
||||||
@ -282,6 +294,18 @@ export class DocumentDetailComponent
|
|||||||
this.router.navigate(['404'])
|
this.router.navigate(['404'])
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.route.paramMap.subscribe((paramMap) => {
|
||||||
|
const section = paramMap.get('section')
|
||||||
|
if (section) {
|
||||||
|
const navIDKey: string = Object.keys(DocumentDetailNavIDs).find(
|
||||||
|
(navID) => navID.toLowerCase() == section
|
||||||
|
)
|
||||||
|
if (navIDKey) {
|
||||||
|
this.activeNavID = DocumentDetailNavIDs[navIDKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -289,6 +313,18 @@ export class DocumentDetailComponent
|
|||||||
this.unsubscribeNotifier.complete()
|
this.unsubscribeNotifier.complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNavChange(navChangeEvent: NgbNavChangeEvent) {
|
||||||
|
const [foundNavIDkey] = Object.entries(DocumentDetailNavIDs).find(
|
||||||
|
([, navIDValue]) => navIDValue == navChangeEvent.nextId
|
||||||
|
)
|
||||||
|
if (foundNavIDkey)
|
||||||
|
this.router.navigate([
|
||||||
|
'documents',
|
||||||
|
this.documentId,
|
||||||
|
foundNavIDkey.toLowerCase(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
updateComponent(doc: PaperlessDocument) {
|
updateComponent(doc: PaperlessDocument) {
|
||||||
this.document = doc
|
this.document = doc
|
||||||
this.requiresPassword = false
|
this.requiresPassword = false
|
||||||
@ -622,9 +658,9 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get commentsEnabled(): boolean {
|
get notesEnabled(): boolean {
|
||||||
return (
|
return (
|
||||||
this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) &&
|
this.settings.get(SETTINGS_KEYS.NOTES_ENABLED) &&
|
||||||
this.permissionsService.currentUserCan(
|
this.permissionsService.currentUserCan(
|
||||||
PermissionAction.View,
|
PermissionAction.View,
|
||||||
PermissionType.Document
|
PermissionType.Document
|
||||||
@ -632,6 +668,11 @@ export class DocumentDetailComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notesUpdated(notes: PaperlessDocumentNote[]) {
|
||||||
|
this.document.notes = notes
|
||||||
|
this.openDocumentService.refreshDocument(this.documentId)
|
||||||
|
}
|
||||||
|
|
||||||
get userIsOwner(): boolean {
|
get userIsOwner(): boolean {
|
||||||
let doc: PaperlessDocument = Object.assign({}, this.document)
|
let doc: PaperlessDocument = Object.assign({}, this.document)
|
||||||
// dont disable while editing
|
// dont disable while editing
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
|
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="list.selectNone()">
|
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
||||||
</svg> <ng-container i18n>Cancel</ng-container>
|
</svg> <ng-container i18n>Cancel</ng-container>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
[items]="tags"
|
[items]="tags"
|
||||||
[disabled]="!userCanEditAll"
|
[disabled]="!userCanEditAll"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[multiple]="true"
|
[manyToOne]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
(opened)="openTagsDropdown()"
|
(opened)="openTagsDropdown()"
|
||||||
[(selectionModel)]="tagSelectionModel"
|
[(selectionModel)]="tagSelectionModel"
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
<span *ngIf="document.__search_hit__ && document.__search_hit__.highlights" [innerHtml]="document.__search_hit__.highlights"></span>
|
<span *ngIf="document.__search_hit__ && document.__search_hit__.highlights" [innerHtml]="document.__search_hit__.highlights"></span>
|
||||||
<span *ngFor="let highlight of searchCommentHighlights" class="d-block">
|
<span *ngFor="let highlight of searchNoteHighlights" class="d-block">
|
||||||
<svg width="1em" height="1em" fill="currentColor" class="me-2">
|
<svg width="1em" height="1em" fill="currentColor" class="me-2">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -65,24 +65,31 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
||||||
|
<button *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="View notes" i18n-title>
|
||||||
|
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||||
|
</svg>
|
||||||
|
<small i18n>{{document.notes.length}} Notes</small>
|
||||||
|
</button>
|
||||||
<button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title
|
<button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by document type" i18n-title
|
||||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||||
<svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
|
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
|
||||||
</svg>
|
</svg>
|
||||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="document.storage_path" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title
|
<button *ngIf="document.storage_path" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path" i18n-title
|
||||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||||
<svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
<use xlink:href="assets/bootstrap-icons.svg#archive"/>
|
||||||
</svg>
|
</svg>
|
||||||
<small>{{(document.storage_path$ | async)?.name}}</small>
|
<small>{{(document.storage_path$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="document.archive_serial_number" class="list-group-item me-2 bg-light text-dark p-1 border-0">
|
<div *ngIf="document.archive_serial_number" class="list-group-item me-2 bg-light text-dark p-1 border-0">
|
||||||
<svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||||
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/>
|
<use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
|
||||||
</svg>
|
</svg>
|
||||||
<small>#{{document.archive_serial_number}}</small>
|
<small>#{{document.archive_serial_number}}</small>
|
||||||
</div>
|
</div>
|
||||||
@ -94,9 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
|
<div class="list-group-item bg-light text-dark p-1 border-0" [ngbTooltip]="dateTooltip">
|
||||||
<svg class="metadata-icon me-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/>
|
<use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
|
||||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,12 +73,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-icon {
|
|
||||||
width: 0.9rem;
|
|
||||||
height: 0.9rem;
|
|
||||||
padding: 0.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-score {
|
.search-score {
|
||||||
padding-top: 0.35rem !important;
|
padding-top: 0.35rem !important;
|
||||||
}
|
}
|
||||||
|
@ -73,16 +73,14 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get searchCommentHighlights() {
|
get searchNoteHighlights() {
|
||||||
let highlights = []
|
let highlights = []
|
||||||
if (
|
if (
|
||||||
this.document['__search_hit__'] &&
|
this.document['__search_hit__'] &&
|
||||||
this.document['__search_hit__'].comment_highlights
|
this.document['__search_hit__'].note_highlights
|
||||||
) {
|
) {
|
||||||
// only show comments with a match
|
// only show notes with a match
|
||||||
highlights = (
|
highlights = (this.document['__search_hit__'].note_highlights as string)
|
||||||
this.document['__search_hit__'].comment_highlights as string
|
|
||||||
)
|
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter((higlight) => higlight.includes('<span'))
|
.filter((higlight) => higlight.includes('<span'))
|
||||||
}
|
}
|
||||||
@ -136,4 +134,8 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
|||||||
(this.document.content.length > 500 ? '...' : '')
|
(this.document.content.length > 500 ? '...' : '')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notesEnabled(): boolean {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,20 @@
|
|||||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||||
<app-tag *ngFor="let t of getTagsLimited$() | async" [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></app-tag>
|
<app-tag *ngFor="let t of getTagsLimited$() | async" [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></app-tag>
|
||||||
<div *ngIf="moreTags">
|
<div *ngIf="moreTags">
|
||||||
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
<span class="badge text-dark">+ {{moreTags}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-2">
|
<a *ngIf="notesEnabled && document.notes.length" routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
|
||||||
|
<span class="badge rounded-pill bg-light border text-primary">
|
||||||
|
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||||
|
</svg>
|
||||||
|
{{document.notes.length}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-body bg-light p-2">
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
<ng-container *ngIf="document.correspondent">
|
<ng-container *ngIf="document.correspondent">
|
||||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
|
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
.doc-img {
|
.doc-img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: top left;
|
object-position: top left;
|
||||||
height: 175px;
|
height: 180px;
|
||||||
mix-blend-mode: multiply;
|
mix-blend-mode: multiply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +34,12 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-card-notes {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 142px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-selected {
|
.card-selected {
|
||||||
border-color:var(--bs-primary);
|
border-color:var(--bs-primary);
|
||||||
|
|
||||||
@ -58,12 +64,6 @@
|
|||||||
color: var(--bs-primary);
|
color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-icon {
|
|
||||||
width: 0.9rem;
|
|
||||||
height: 0.9rem;
|
|
||||||
padding: 0.05rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer .btn {
|
.card-footer .btn {
|
||||||
|
@ -74,11 +74,12 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTagsLimited$() {
|
getTagsLimited$() {
|
||||||
|
const limit = this.document.notes.length > 0 ? 6 : 7
|
||||||
return this.document.tags$.pipe(
|
return this.document.tags$.pipe(
|
||||||
map((tags) => {
|
map((tags) => {
|
||||||
if (tags.length > 7) {
|
if (tags.length > limit) {
|
||||||
this.moreTags = tags.length - 6
|
this.moreTags = tags.length - (limit - 1)
|
||||||
return tags.slice(0, 6)
|
return tags.slice(0, limit - 1)
|
||||||
} else {
|
} else {
|
||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
@ -110,4 +111,8 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
|||||||
mouseLeaveCard() {
|
mouseLeaveCard() {
|
||||||
this.popover.close()
|
this.popover.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notesEnabled(): boolean {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,19 +14,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group flex-fill" role="group">
|
<div class="btn-group flex-fill" role="group">
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails">
|
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
||||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall">
|
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
||||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge">
|
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
||||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
||||||
@ -123,42 +123,56 @@
|
|||||||
<th></th>
|
<th></th>
|
||||||
<th class="d-none d-lg-table-cell"
|
<th class="d-none d-lg-table-cell"
|
||||||
appSortable="archive_serial_number"
|
appSortable="archive_serial_number"
|
||||||
|
title="Sort by ASN" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>ASN</th>
|
i18n>ASN</th>
|
||||||
<th class="d-none d-md-table-cell"
|
<th class="d-none d-md-table-cell"
|
||||||
appSortable="correspondent__name"
|
appSortable="correspondent__name"
|
||||||
|
title="Sort by correspondent" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Correspondent</th>
|
i18n>Correspondent</th>
|
||||||
<th
|
<th
|
||||||
appSortable="title"
|
appSortable="title"
|
||||||
|
title="Sort by title" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Title</th>
|
i18n>Title</th>
|
||||||
|
<th *ngIf="notesEnabled" class="d-none d-xl-table-cell"
|
||||||
|
appSortable="num_notes"
|
||||||
|
title="Sort by notes" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Notes</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
appSortable="document_type__name"
|
appSortable="document_type__name"
|
||||||
|
title="Sort by document type" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Document type</th>
|
i18n>Document type</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
appSortable="storage_path__name"
|
appSortable="storage_path__name"
|
||||||
|
title="Sort by storage path" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Storage path</th>
|
i18n>Storage path</th>
|
||||||
<th
|
<th
|
||||||
appSortable="created"
|
appSortable="created"
|
||||||
|
title="Sort by created date" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Created</th>
|
i18n>Created</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
appSortable="added"
|
appSortable="added"
|
||||||
|
title="Sort by added date" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
@ -184,6 +198,15 @@
|
|||||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
||||||
</td>
|
</td>
|
||||||
|
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell">
|
||||||
|
<a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
|
||||||
|
<span class="badge rounded-pill bg-light border text-primary">
|
||||||
|
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||||
|
</svg>
|
||||||
|
{{d.notes.length}}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
<ng-container *ngIf="d.document_type">
|
<ng-container *ngIf="d.document_type">
|
||||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
||||||
|
@ -6,6 +6,10 @@ tr {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.table-row-selected {
|
.table-row-selected {
|
||||||
background-color: var(--pngx-primary-faded);
|
background-color: var(--pngx-primary-faded);
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
import {
|
import {
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
SortEvent,
|
SortEvent,
|
||||||
@ -29,6 +30,7 @@ import {
|
|||||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||||
} from 'src/app/services/rest/document.service'
|
} from 'src/app/services/rest/document.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.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 { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||||
@ -51,7 +53,8 @@ export class DocumentListComponent
|
|||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private consumerStatusService: ConsumerStatusService,
|
private consumerStatusService: ConsumerStatusService,
|
||||||
public openDocumentsService: OpenDocumentsService
|
public openDocumentsService: OpenDocumentsService,
|
||||||
|
private settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -289,4 +292,8 @@ export class DocumentListComponent
|
|||||||
trackByDocumentId(index, item: PaperlessDocument) {
|
trackByDocumentId(index, item: PaperlessDocument) {
|
||||||
return item.id
|
return item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notesEnabled(): boolean {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
|
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
|
||||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
[items]="tags"
|
[items]="tags"
|
||||||
[multiple]="true"
|
[manyToOne]="true"
|
||||||
[(selectionModel)]="tagSelectionModel"
|
[(selectionModel)]="tagSelectionModel"
|
||||||
(selectionModelChange)="updateRules()"
|
(selectionModelChange)="updateRules()"
|
||||||
(opened)="onTagsDropdownOpen()"
|
(opened)="onTagsDropdownOpen()"
|
||||||
|
@ -21,10 +21,10 @@ import {
|
|||||||
FILTER_ADDED_AFTER,
|
FILTER_ADDED_AFTER,
|
||||||
FILTER_ADDED_BEFORE,
|
FILTER_ADDED_BEFORE,
|
||||||
FILTER_ASN,
|
FILTER_ASN,
|
||||||
FILTER_CORRESPONDENT,
|
FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
FILTER_CREATED_AFTER,
|
FILTER_CREATED_AFTER,
|
||||||
FILTER_CREATED_BEFORE,
|
FILTER_CREATED_BEFORE,
|
||||||
FILTER_DOCUMENT_TYPE,
|
FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||||
FILTER_FULLTEXT_MORELIKE,
|
FILTER_FULLTEXT_MORELIKE,
|
||||||
FILTER_FULLTEXT_QUERY,
|
FILTER_FULLTEXT_QUERY,
|
||||||
FILTER_HAS_ANY_TAG,
|
FILTER_HAS_ANY_TAG,
|
||||||
@ -33,12 +33,22 @@ import {
|
|||||||
FILTER_DOES_NOT_HAVE_TAG,
|
FILTER_DOES_NOT_HAVE_TAG,
|
||||||
FILTER_TITLE,
|
FILTER_TITLE,
|
||||||
FILTER_TITLE_CONTENT,
|
FILTER_TITLE_CONTENT,
|
||||||
FILTER_STORAGE_PATH,
|
FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
FILTER_ASN_ISNULL,
|
FILTER_ASN_ISNULL,
|
||||||
FILTER_ASN_GT,
|
FILTER_ASN_GT,
|
||||||
FILTER_ASN_LT,
|
FILTER_ASN_LT,
|
||||||
|
FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||||
|
FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||||
|
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
FILTER_DOCUMENT_TYPE,
|
||||||
|
FILTER_CORRESPONDENT,
|
||||||
|
FILTER_STORAGE_PATH,
|
||||||
} from 'src/app/data/filter-rule-type'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component'
|
import {
|
||||||
|
FilterableDropdownSelectionModel,
|
||||||
|
Intersection,
|
||||||
|
LogicalOperator,
|
||||||
|
} from '../../common/filterable-dropdown/filterable-dropdown.component'
|
||||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
import {
|
import {
|
||||||
DocumentService,
|
DocumentService,
|
||||||
@ -93,7 +103,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
if (this.filterRules.length == 1) {
|
if (this.filterRules.length == 1) {
|
||||||
let rule = this.filterRules[0]
|
let rule = this.filterRules[0]
|
||||||
switch (this.filterRules[0].rule_type) {
|
switch (this.filterRules[0].rule_type) {
|
||||||
case FILTER_CORRESPONDENT:
|
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||||
if (rule.value) {
|
if (rule.value) {
|
||||||
return $localize`Correspondent: ${
|
return $localize`Correspondent: ${
|
||||||
this.correspondents.find((c) => c.id == +rule.value)?.name
|
this.correspondents.find((c) => c.id == +rule.value)?.name
|
||||||
@ -102,7 +112,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
return $localize`Without correspondent`
|
return $localize`Without correspondent`
|
||||||
}
|
}
|
||||||
|
|
||||||
case FILTER_DOCUMENT_TYPE:
|
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||||
if (rule.value) {
|
if (rule.value) {
|
||||||
return $localize`Type: ${
|
return $localize`Type: ${
|
||||||
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
|
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
|
||||||
@ -335,6 +345,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
this.dateAddedBefore = rule.value
|
this.dateAddedBefore = rule.value
|
||||||
break
|
break
|
||||||
case FILTER_HAS_TAGS_ALL:
|
case FILTER_HAS_TAGS_ALL:
|
||||||
|
this.tagSelectionModel.logicalOperator = LogicalOperator.And
|
||||||
this.tagSelectionModel.set(
|
this.tagSelectionModel.set(
|
||||||
rule.value ? +rule.value : null,
|
rule.value ? +rule.value : null,
|
||||||
ToggleableItemState.Selected,
|
ToggleableItemState.Selected,
|
||||||
@ -342,7 +353,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
case FILTER_HAS_TAGS_ANY:
|
case FILTER_HAS_TAGS_ANY:
|
||||||
this.tagSelectionModel.logicalOperator = 'or'
|
this.tagSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
this.tagSelectionModel.set(
|
this.tagSelectionModel.set(
|
||||||
rule.value ? +rule.value : null,
|
rule.value ? +rule.value : null,
|
||||||
ToggleableItemState.Selected,
|
ToggleableItemState.Selected,
|
||||||
@ -360,26 +371,59 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
case FILTER_CORRESPONDENT:
|
case FILTER_CORRESPONDENT:
|
||||||
|
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||||
|
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.correspondentSelectionModel.intersection = Intersection.Include
|
||||||
this.correspondentSelectionModel.set(
|
this.correspondentSelectionModel.set(
|
||||||
rule.value ? +rule.value : null,
|
rule.value ? +rule.value : null,
|
||||||
ToggleableItemState.Selected,
|
ToggleableItemState.Selected,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_CORRESPONDENT:
|
||||||
|
this.correspondentSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.correspondentSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
case FILTER_DOCUMENT_TYPE:
|
case FILTER_DOCUMENT_TYPE:
|
||||||
|
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||||
|
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.documentTypeSelectionModel.intersection = Intersection.Include
|
||||||
this.documentTypeSelectionModel.set(
|
this.documentTypeSelectionModel.set(
|
||||||
rule.value ? +rule.value : null,
|
rule.value ? +rule.value : null,
|
||||||
ToggleableItemState.Selected,
|
ToggleableItemState.Selected,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE:
|
||||||
|
this.documentTypeSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.documentTypeSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
case FILTER_STORAGE_PATH:
|
case FILTER_STORAGE_PATH:
|
||||||
|
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||||
|
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.storagePathSelectionModel.intersection = Intersection.Include
|
||||||
this.storagePathSelectionModel.set(
|
this.storagePathSelectionModel.set(
|
||||||
rule.value ? +rule.value : null,
|
rule.value ? +rule.value : null,
|
||||||
ToggleableItemState.Selected,
|
ToggleableItemState.Selected,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_STORAGE_PATH:
|
||||||
|
this.storagePathSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.storagePathSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
case FILTER_ASN_ISNULL:
|
case FILTER_ASN_ISNULL:
|
||||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
this.textFilterModifier =
|
this.textFilterModifier =
|
||||||
@ -469,7 +513,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' })
|
filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' })
|
||||||
} else {
|
} else {
|
||||||
const tagFilterType =
|
const tagFilterType =
|
||||||
this.tagSelectionModel.logicalOperator == 'and'
|
this.tagSelectionModel.logicalOperator == LogicalOperator.And
|
||||||
? FILTER_HAS_TAGS_ALL
|
? FILTER_HAS_TAGS_ALL
|
||||||
: FILTER_HAS_TAGS_ANY
|
: FILTER_HAS_TAGS_ANY
|
||||||
this.tagSelectionModel
|
this.tagSelectionModel
|
||||||
@ -491,28 +535,66 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.correspondentSelectionModel
|
if (this.correspondentSelectionModel.isNoneSelected()) {
|
||||||
.getSelectedItems()
|
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
|
||||||
.forEach((correspondent) => {
|
} else {
|
||||||
filterRules.push({
|
this.correspondentSelectionModel
|
||||||
rule_type: FILTER_CORRESPONDENT,
|
.getSelectedItems()
|
||||||
value: correspondent.id?.toString(),
|
.forEach((correspondent) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
|
value: correspondent.id?.toString(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
this.correspondentSelectionModel
|
||||||
this.documentTypeSelectionModel
|
.getExcludedItems()
|
||||||
.getSelectedItems()
|
.forEach((correspondent) => {
|
||||||
.forEach((documentType) => {
|
filterRules.push({
|
||||||
filterRules.push({
|
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||||
rule_type: FILTER_DOCUMENT_TYPE,
|
value: correspondent.id?.toString(),
|
||||||
value: documentType.id?.toString(),
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => {
|
if (this.documentTypeSelectionModel.isNoneSelected()) {
|
||||||
filterRules.push({
|
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
|
||||||
rule_type: FILTER_STORAGE_PATH,
|
} else {
|
||||||
value: storagePath.id?.toString(),
|
this.documentTypeSelectionModel
|
||||||
})
|
.getSelectedItems()
|
||||||
})
|
.forEach((documentType) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||||
|
value: documentType.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.documentTypeSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.forEach((documentType) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||||
|
value: documentType.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.storagePathSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
||||||
|
} else {
|
||||||
|
this.storagePathSelectionModel
|
||||||
|
.getSelectedItems()
|
||||||
|
.forEach((storagePath) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
|
value: storagePath.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.storagePathSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.forEach((storagePath) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
value: storagePath.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
if (this.dateCreatedBefore) {
|
if (this.dateCreatedBefore) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_CREATED_BEFORE,
|
rule_type: FILTER_CREATED_BEFORE,
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
<div *ngIf="comments">
|
<div *ngIf="notes">
|
||||||
<form [formGroup]="commentForm" class="needs-validation mt-3" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Comment }" novalidate>
|
<form [formGroup]="noteForm" class="needs-validation mt-3" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Note }" novalidate>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea class="form-control form-control-sm" [class.is-invalid]="newCommentError" rows="3" formControlName="newComment" placeholder="Enter comment" i18n-placeholder required></textarea>
|
<textarea class="form-control form-control-sm" [class.is-invalid]="newNoteError" rows="3" formControlName="newNote" placeholder="Enter note" i18n-placeholder (keydown)="noteFormKeydown($event)" required></textarea>
|
||||||
<div class="invalid-feedback" i18n>
|
<div class="invalid-feedback" i18n>
|
||||||
Please enter a comment.
|
Please enter a note.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mt-2 d-flex justify-content-end align-items-center">
|
<div class="form-group mt-2 d-flex justify-content-end align-items-center">
|
||||||
<div *ngIf="networkActive" class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div>
|
<div *ngIf="networkActive" class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div>
|
||||||
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive" (click)="addComment()" i18n>Add comment</button>
|
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive" (click)="addNote()" i18n>Add note</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<hr>
|
<hr>
|
||||||
<div *ngFor="let comment of comments" class="card border mb-3">
|
<div *ngFor="let note of notes" class="card border mb-3">
|
||||||
<div class="card-body text-dark">
|
<div class="card-body text-dark">
|
||||||
<p class="card-text">{{comment.comment}}</p>
|
<p class="card-text">{{note.note}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
|
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
|
||||||
<span>{{displayName(comment)}} - {{ comment.created | customDate}}</span>
|
<span>{{displayName(note)}} - {{ note.created | customDate}}</span>
|
||||||
<button type="button" class="btn btn-link btn-sm p-0 fade" (click)="deleteComment(comment.id)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Comment }">
|
<button type="button" class="btn btn-link btn-sm p-0 fade" title="Delete note" i18n-title (click)="deleteNote(note.id)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Note }">
|
||||||
<svg width="13" height="13" fill="currentColor">
|
<svg width="13" height="13" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<span class="visually-hidden" i18n>Delete note</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,106 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { DocumentNotesService } from 'src/app/services/rest/document-notes.service'
|
||||||
|
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { first } from 'rxjs/operators'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-document-notes',
|
||||||
|
templateUrl: './document-notes.component.html',
|
||||||
|
styleUrls: ['./document-notes.component.scss'],
|
||||||
|
})
|
||||||
|
export class DocumentNotesComponent extends ComponentWithPermissions {
|
||||||
|
noteForm: FormGroup = new FormGroup({
|
||||||
|
newNote: new FormControl(''),
|
||||||
|
})
|
||||||
|
|
||||||
|
networkActive = false
|
||||||
|
newNoteError: boolean = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
documentId: number
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
notes: PaperlessDocumentNote[] = []
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
updated: EventEmitter<PaperlessDocumentNote[]> = new EventEmitter()
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private notesService: DocumentNotesService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private usersService: UserService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.usersService.listAll().subscribe({
|
||||||
|
next: (users) => {
|
||||||
|
this.users = users.results
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addNote() {
|
||||||
|
const note: string = this.noteForm.get('newNote').value.toString().trim()
|
||||||
|
if (note.length == 0) {
|
||||||
|
this.newNoteError = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.newNoteError = false
|
||||||
|
this.networkActive = true
|
||||||
|
this.notesService.addNote(this.documentId, note).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.notes = result
|
||||||
|
this.noteForm.get('newNote').reset()
|
||||||
|
this.networkActive = false
|
||||||
|
this.updated.emit(this.notes)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.networkActive = false
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error saving note: ${e.toString()}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNote(noteId: number) {
|
||||||
|
this.notesService.deleteNote(this.documentId, noteId).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.notes = result
|
||||||
|
this.networkActive = false
|
||||||
|
this.updated.emit(this.notes)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.networkActive = false
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error deleting note: ${e.toString()}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName(note: PaperlessDocumentNote): string {
|
||||||
|
if (!note.user) return ''
|
||||||
|
const user = this.users.find((u) => u.id === note.user)
|
||||||
|
if (!user) return ''
|
||||||
|
const nameComponents = []
|
||||||
|
if (user.first_name) nameComponents.unshift(user.first_name)
|
||||||
|
if (user.last_name) nameComponents.unshift(user.last_name)
|
||||||
|
if (user.username) {
|
||||||
|
if (nameComponents.length > 0) nameComponents.push(`(${user.username})`)
|
||||||
|
else nameComponents.push(user.username)
|
||||||
|
}
|
||||||
|
return nameComponents.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
noteFormKeydown(event: KeyboardEvent) {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||||
|
this.addNote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
@ -35,7 +35,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
|||||||
toastService,
|
toastService,
|
||||||
documentListViewService,
|
documentListViewService,
|
||||||
permissionsService,
|
permissionsService,
|
||||||
FILTER_CORRESPONDENT,
|
FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
$localize`correspondent`,
|
$localize`correspondent`,
|
||||||
$localize`correspondents`,
|
$localize`correspondents`,
|
||||||
PermissionType.Correspondent,
|
PermissionType.Correspondent,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import {
|
import {
|
||||||
@ -32,7 +32,7 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
|||||||
toastService,
|
toastService,
|
||||||
documentListViewService,
|
documentListViewService,
|
||||||
permissionsService,
|
permissionsService,
|
||||||
FILTER_DOCUMENT_TYPE,
|
FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||||
$localize`document type`,
|
$localize`document type`,
|
||||||
$localize`document types`,
|
$localize`document types`,
|
||||||
PermissionType.DocumentType,
|
PermissionType.DocumentType,
|
||||||
|
@ -167,8 +167,13 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
activeModal.componentInstance.succeeded.subscribe({
|
activeModal.componentInstance.succeeded.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
if (activeModal.componentInstance.error) {
|
if (activeModal.componentInstance.error) {
|
||||||
|
const errorDetail = activeModal.componentInstance.error.error
|
||||||
|
? activeModal.componentInstance.error.error[0]
|
||||||
|
: null
|
||||||
this.toastService.showInfo(
|
this.toastService.showInfo(
|
||||||
$localize`Error occurred while saving ${this.typeName} : ${activeModal.componentInstance.error}.`
|
$localize`Error occurred while saving ${this.typeName}${
|
||||||
|
errorDetail ? ': ' + errorDetail : ''
|
||||||
|
}.`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
|
@ -156,11 +156,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="mt-4" i18n>Comments</h4>
|
<h4 class="mt-4" i18n>Notes</h4>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-md-3 col">
|
<div class="offset-md-3 col">
|
||||||
<app-input-check i18n-title title="Enable comments" formControlName="commentsEnabled"></app-input-check>
|
<app-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ export class SettingsComponent
|
|||||||
displayLanguage: new FormControl(null),
|
displayLanguage: new FormControl(null),
|
||||||
dateLocale: new FormControl(null),
|
dateLocale: new FormControl(null),
|
||||||
dateFormat: new FormControl(null),
|
dateFormat: new FormControl(null),
|
||||||
commentsEnabled: new FormControl(null),
|
notesEnabled: new FormControl(null),
|
||||||
updateCheckingEnabled: new FormControl(null),
|
updateCheckingEnabled: new FormControl(null),
|
||||||
|
|
||||||
notificationsConsumerNewDocument: new FormControl(null),
|
notificationsConsumerNewDocument: new FormControl(null),
|
||||||
@ -196,7 +196,7 @@ export class SettingsComponent
|
|||||||
displayLanguage: this.settings.getLanguage(),
|
displayLanguage: this.settings.getLanguage(),
|
||||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||||
commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
|
notesEnabled: this.settings.get(SETTINGS_KEYS.NOTES_ENABLED),
|
||||||
updateCheckingEnabled: this.settings.get(
|
updateCheckingEnabled: this.settings.get(
|
||||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
|
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
|
||||||
),
|
),
|
||||||
@ -552,8 +552,8 @@ export class SettingsComponent
|
|||||||
this.settingsForm.value.notificationsConsumerSuppressOnDashboard
|
this.settingsForm.value.notificationsConsumerSuppressOnDashboard
|
||||||
)
|
)
|
||||||
this.settings.set(
|
this.settings.set(
|
||||||
SETTINGS_KEYS.COMMENTS_ENABLED,
|
SETTINGS_KEYS.NOTES_ENABLED,
|
||||||
this.settingsForm.value.commentsEnabled
|
this.settingsForm.value.notesEnabled
|
||||||
)
|
)
|
||||||
this.settings.set(
|
this.settings.set(
|
||||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import {
|
import {
|
||||||
@ -32,7 +32,7 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS
|
|||||||
toastService,
|
toastService,
|
||||||
documentListViewService,
|
documentListViewService,
|
||||||
permissionsService,
|
permissionsService,
|
||||||
FILTER_STORAGE_PATH,
|
FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
$localize`storage path`,
|
$localize`storage path`,
|
||||||
$localize`storage paths`,
|
$localize`storage paths`,
|
||||||
PermissionType.StoragePath,
|
PermissionType.StoragePath,
|
||||||
|
@ -8,8 +8,12 @@ export const FILTER_ASN_GT = 23
|
|||||||
export const FILTER_ASN_LT = 24
|
export const FILTER_ASN_LT = 24
|
||||||
|
|
||||||
export const FILTER_CORRESPONDENT = 3
|
export const FILTER_CORRESPONDENT = 3
|
||||||
|
export const FILTER_HAS_CORRESPONDENT_ANY = 26
|
||||||
|
export const FILTER_DOES_NOT_HAVE_CORRESPONDENT = 27
|
||||||
|
|
||||||
export const FILTER_DOCUMENT_TYPE = 4
|
export const FILTER_DOCUMENT_TYPE = 4
|
||||||
|
export const FILTER_HAS_DOCUMENT_TYPE_ANY = 28
|
||||||
|
export const FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE = 29
|
||||||
|
|
||||||
export const FILTER_IS_IN_INBOX = 5
|
export const FILTER_IS_IN_INBOX = 5
|
||||||
export const FILTER_HAS_TAGS_ALL = 6
|
export const FILTER_HAS_TAGS_ALL = 6
|
||||||
@ -18,6 +22,8 @@ export const FILTER_DOES_NOT_HAVE_TAG = 17
|
|||||||
export const FILTER_HAS_TAGS_ANY = 22
|
export const FILTER_HAS_TAGS_ANY = 22
|
||||||
|
|
||||||
export const FILTER_STORAGE_PATH = 25
|
export const FILTER_STORAGE_PATH = 25
|
||||||
|
export const FILTER_HAS_STORAGE_PATH_ANY = 30
|
||||||
|
export const FILTER_DOES_NOT_HAVE_STORAGE_PATH = 31
|
||||||
|
|
||||||
export const FILTER_CREATED_BEFORE = 8
|
export const FILTER_CREATED_BEFORE = 8
|
||||||
export const FILTER_CREATED_AFTER = 9
|
export const FILTER_CREATED_AFTER = 9
|
||||||
@ -63,6 +69,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
|||||||
datatype: 'correspondent',
|
datatype: 'correspondent',
|
||||||
multi: false,
|
multi: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
|
filtervar: 'correspondent__id__in',
|
||||||
|
datatype: 'correspondent',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||||
|
filtervar: 'correspondent__id__none',
|
||||||
|
datatype: 'correspondent',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: FILTER_STORAGE_PATH,
|
id: FILTER_STORAGE_PATH,
|
||||||
filtervar: 'storage_path__id',
|
filtervar: 'storage_path__id',
|
||||||
@ -70,6 +88,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
|||||||
datatype: 'storage_path',
|
datatype: 'storage_path',
|
||||||
multi: false,
|
multi: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
|
filtervar: 'storage_path__id__in',
|
||||||
|
datatype: 'storage_path',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
filtervar: 'storage_path__id__none',
|
||||||
|
datatype: 'storage_path',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: FILTER_DOCUMENT_TYPE,
|
id: FILTER_DOCUMENT_TYPE,
|
||||||
filtervar: 'document_type__id',
|
filtervar: 'document_type__id',
|
||||||
@ -77,6 +107,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
|||||||
datatype: 'document_type',
|
datatype: 'document_type',
|
||||||
multi: false,
|
multi: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||||
|
filtervar: 'document_type__id__in',
|
||||||
|
datatype: 'document_type',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||||
|
filtervar: 'document_type__id__none',
|
||||||
|
datatype: 'document_type',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: FILTER_IS_IN_INBOX,
|
id: FILTER_IS_IN_INBOX,
|
||||||
filtervar: 'is_in_inbox',
|
filtervar: 'is_in_inbox',
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { ObjectWithId } from './object-with-id'
|
|
||||||
import { PaperlessUser } from './paperless-user'
|
|
||||||
|
|
||||||
export interface PaperlessDocumentComment extends ObjectWithId {
|
|
||||||
created?: Date
|
|
||||||
comment?: string
|
|
||||||
user?: PaperlessUser
|
|
||||||
}
|
|
7
src-ui/src/app/data/paperless-document-note.ts
Normal file
7
src-ui/src/app/data/paperless-document-note.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { ObjectWithId } from './object-with-id'
|
||||||
|
|
||||||
|
export interface PaperlessDocumentNote extends ObjectWithId {
|
||||||
|
created?: Date
|
||||||
|
note?: string
|
||||||
|
user?: number // PaperlessUser
|
||||||
|
}
|
@ -4,13 +4,14 @@ import { PaperlessDocumentType } from './paperless-document-type'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { PaperlessStoragePath } from './paperless-storage-path'
|
import { PaperlessStoragePath } from './paperless-storage-path'
|
||||||
import { ObjectWithPermissions } from './object-with-permissions'
|
import { ObjectWithPermissions } from './object-with-permissions'
|
||||||
|
import { PaperlessDocumentNote } from './paperless-document-note'
|
||||||
|
|
||||||
export interface SearchHit {
|
export interface SearchHit {
|
||||||
score?: number
|
score?: number
|
||||||
rank?: number
|
rank?: number
|
||||||
|
|
||||||
highlights?: string
|
highlights?: string
|
||||||
comment_highlights?: string
|
note_highlights?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaperlessDocument extends ObjectWithPermissions {
|
export interface PaperlessDocument extends ObjectWithPermissions {
|
||||||
@ -54,5 +55,7 @@ export interface PaperlessDocument extends ObjectWithPermissions {
|
|||||||
|
|
||||||
archive_serial_number?: number
|
archive_serial_number?: number
|
||||||
|
|
||||||
|
notes?: PaperlessDocumentNote[]
|
||||||
|
|
||||||
__search_hit__?: SearchHit
|
__search_hit__?: SearchHit
|
||||||
}
|
}
|
||||||
|
@ -20,4 +20,6 @@ export interface PaperlessMailAccount extends ObjectWithId {
|
|||||||
password: string
|
password: string
|
||||||
|
|
||||||
character_set?: string
|
character_set?: string
|
||||||
|
|
||||||
|
is_token: boolean
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export const SETTINGS_KEYS = {
|
|||||||
'general-settings:notifications:consumer-failed',
|
'general-settings:notifications:consumer-failed',
|
||||||
NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD:
|
NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD:
|
||||||
'general-settings:notifications:consumer-suppress-on-dashboard',
|
'general-settings:notifications:consumer-suppress-on-dashboard',
|
||||||
COMMENTS_ENABLED: 'general-settings:comments-enabled',
|
NOTES_ENABLED: 'general-settings:notes-enabled',
|
||||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||||
UPDATE_CHECKING_BACKEND_SETTING:
|
UPDATE_CHECKING_BACKEND_SETTING:
|
||||||
@ -125,7 +125,7 @@ export const SETTINGS: PaperlessUiSetting[] = [
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.COMMENTS_ENABLED,
|
key: SETTINGS_KEYS.NOTES_ENABLED,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
@ -35,15 +35,16 @@ export class OpenDocumentsService {
|
|||||||
refreshDocument(id: number) {
|
refreshDocument(id: number) {
|
||||||
let index = this.openDocuments.findIndex((doc) => doc.id == id)
|
let index = this.openDocuments.findIndex((doc) => doc.id == id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.documentService.get(id).subscribe(
|
this.documentService.get(id).subscribe({
|
||||||
(doc) => {
|
next: (doc) => {
|
||||||
this.openDocuments[index] = doc
|
this.openDocuments[index] = doc
|
||||||
|
this.save()
|
||||||
},
|
},
|
||||||
(error) => {
|
error: () => {
|
||||||
this.openDocuments.splice(index, 1)
|
this.openDocuments.splice(index, 1)
|
||||||
this.save()
|
this.save()
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export enum PermissionType {
|
|||||||
SavedView = '%s_savedview',
|
SavedView = '%s_savedview',
|
||||||
PaperlessTask = '%s_paperlesstask',
|
PaperlessTask = '%s_paperlesstask',
|
||||||
UISettings = '%s_uisettings',
|
UISettings = '%s_uisettings',
|
||||||
Comment = '%s_comment',
|
Note = '%s_note',
|
||||||
MailAccount = '%s_mailaccount',
|
MailAccount = '%s_mailaccount',
|
||||||
MailRule = '%s_mailrule',
|
MailRule = '%s_mailrule',
|
||||||
User = '%s_user',
|
User = '%s_user',
|
||||||
|
@ -2,10 +2,8 @@ import { HttpClient, HttpParams } from '@angular/common/http'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { map, publishReplay, refCount } from 'rxjs/operators'
|
import { map, publishReplay, refCount } from 'rxjs/operators'
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
import { PaperlessUser } from 'src/app/data/paperless-user'
|
|
||||||
import { Results } from 'src/app/data/results'
|
import { Results } from 'src/app/data/results'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { PermissionAction, PermissionType } from '../permissions.service'
|
|
||||||
|
|
||||||
export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
||||||
protected baseUrl: string = environment.apiBaseUrl
|
protected baseUrl: string = environment.apiBaseUrl
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
|
||||||
import { PaperlessDocumentComment } from 'src/app/data/paperless-document-comment'
|
|
||||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
|
||||||
import { Observable } from 'rxjs'
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class DocumentCommentsService extends AbstractPaperlessService<PaperlessDocumentComment> {
|
|
||||||
constructor(http: HttpClient) {
|
|
||||||
super(http, 'documents')
|
|
||||||
}
|
|
||||||
|
|
||||||
getComments(documentId: number): Observable<PaperlessDocumentComment[]> {
|
|
||||||
return this.http.get<PaperlessDocumentComment[]>(
|
|
||||||
this.getResourceUrl(documentId, 'comments')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addComment(id: number, comment): Observable<PaperlessDocumentComment[]> {
|
|
||||||
return this.http.post<PaperlessDocumentComment[]>(
|
|
||||||
this.getResourceUrl(id, 'comments'),
|
|
||||||
{ comment: comment }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteComment(
|
|
||||||
documentId: number,
|
|
||||||
commentId: number
|
|
||||||
): Observable<PaperlessDocumentComment[]> {
|
|
||||||
return this.http.delete<PaperlessDocumentComment[]>(
|
|
||||||
this.getResourceUrl(documentId, 'comments'),
|
|
||||||
{ params: new HttpParams({ fromString: `id=${commentId}` }) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
37
src-ui/src/app/services/rest/document-notes.service.ts
Normal file
37
src-ui/src/app/services/rest/document-notes.service.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
|
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
|
||||||
|
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class DocumentNotesService extends AbstractPaperlessService<PaperlessDocumentNote> {
|
||||||
|
constructor(http: HttpClient) {
|
||||||
|
super(http, 'documents')
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotes(documentId: number): Observable<PaperlessDocumentNote[]> {
|
||||||
|
return this.http.get<PaperlessDocumentNote[]>(
|
||||||
|
this.getResourceUrl(documentId, 'notes')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addNote(id: number, note: string): Observable<PaperlessDocumentNote[]> {
|
||||||
|
return this.http.post<PaperlessDocumentNote[]>(
|
||||||
|
this.getResourceUrl(id, 'notes'),
|
||||||
|
{ note: note }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNote(
|
||||||
|
documentId: number,
|
||||||
|
noteId: number
|
||||||
|
): Observable<PaperlessDocumentNote[]> {
|
||||||
|
return this.http.delete<PaperlessDocumentNote[]>(
|
||||||
|
this.getResourceUrl(documentId, 'notes'),
|
||||||
|
{ params: new HttpParams({ fromString: `id=${noteId}` }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ export const DOCUMENT_SORT_FIELDS = [
|
|||||||
{ field: 'created', name: $localize`Created` },
|
{ field: 'created', name: $localize`Created` },
|
||||||
{ field: 'added', name: $localize`Added` },
|
{ field: 'added', name: $localize`Added` },
|
||||||
{ field: 'modified', name: $localize`Modified` },
|
{ field: 'modified', name: $localize`Modified` },
|
||||||
|
{ field: 'num_notes', name: $localize`Notes` },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [
|
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [
|
||||||
|
@ -48,4 +48,8 @@ export class MailAccountService extends AbstractPaperlessService<PaperlessMailAc
|
|||||||
delete(o: PaperlessMailAccount) {
|
delete(o: PaperlessMailAccount) {
|
||||||
return super.delete(o).pipe(tap(() => this.reload()))
|
return super.delete(o).pipe(tap(() => this.reload()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test(o: PaperlessMailAccount) {
|
||||||
|
return this.http.post(this.getResourceUrl() + 'test/', o)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,12 +86,12 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
|
|||||||
let params = {}
|
let params = {}
|
||||||
for (let rule of filterRules) {
|
for (let rule of filterRules) {
|
||||||
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
|
||||||
if (ruleType.multi) {
|
if (ruleType.isnull_filtervar && rule.value == null) {
|
||||||
|
params[ruleType.isnull_filtervar] = 1
|
||||||
|
} else if (ruleType.multi) {
|
||||||
params[ruleType.filtervar] = params[ruleType.filtervar]
|
params[ruleType.filtervar] = params[ruleType.filtervar]
|
||||||
? params[ruleType.filtervar] + ',' + rule.value
|
? params[ruleType.filtervar] + ',' + rule.value
|
||||||
: rule.value
|
: rule.value
|
||||||
} else if (ruleType.isnull_filtervar && rule.value == null) {
|
|
||||||
params[ruleType.isnull_filtervar] = 1
|
|
||||||
} else {
|
} else {
|
||||||
params[ruleType.filtervar] = rule.value
|
params[ruleType.filtervar] = rule.value
|
||||||
if (ruleType.datatype == 'boolean')
|
if (ruleType.datatype == 'boolean')
|
||||||
|
@ -436,6 +436,12 @@ textarea,
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metadata-icon {
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 0.9rem;
|
||||||
|
padding: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
table.table {
|
table.table {
|
||||||
color: var(--bs-body-color);
|
color: var(--bs-body-color);
|
||||||
|
|
||||||
@ -623,3 +629,7 @@ code {
|
|||||||
.accordion-button::after {
|
.accordion-button::after {
|
||||||
filter: invert(0.5) saturate(0);
|
filter: invert(0.5) saturate(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.me-1px {
|
||||||
|
margin-right: 1px !important;
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ body {
|
|||||||
--pngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%));
|
--pngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%));
|
||||||
--pngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%));
|
--pngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%));
|
||||||
--pngx-primary-darken-27: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 27%));
|
--pngx-primary-darken-27: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 27%));
|
||||||
|
--pngx-success-darken-10: hsl(152, 69%, 11%); // based on success #198754
|
||||||
--pngx-bg-alt: #fff;
|
--pngx-bg-alt: #fff;
|
||||||
--pngx-bg-darker: var(--bs-gray-100);
|
--pngx-bg-darker: var(--bs-gray-100);
|
||||||
--pngx-focus-alpha: 0.3;
|
--pngx-focus-alpha: 0.3;
|
||||||
@ -133,6 +134,14 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
filter: brightness(.8);
|
filter: brightness(.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge.bg-light.border {
|
||||||
|
border-color: rgba(0,0,0,0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-card .card-body.bg-light {
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.doc-img {
|
.doc-img {
|
||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -197,6 +206,12 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
--bs-alert-border-color: var(--pngx-bg-darker);
|
--bs-alert-border-color: var(--pngx-bg-darker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
--bs-alert-color: var(--pngx-body-color-accent);
|
||||||
|
--bs-alert-bg: var(--pngx-success-darken-10);
|
||||||
|
--bs-alert-border-color: var(--pngx-bg-darker);
|
||||||
|
}
|
||||||
|
|
||||||
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
||||||
color: var(--pngx-body-color-accent);
|
color: var(--pngx-body-color-accent);
|
||||||
}
|
}
|
||||||
@ -223,6 +238,18 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
--bs-dropdown-color: var(--bs-body-color);
|
--bs-dropdown-color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card .list-group-item {
|
||||||
|
--bs-border-color: rgb(var(--bs-dark-rgb));
|
||||||
|
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: rgb(var(--bs-dark-rgb)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-sticky.bg-white {
|
||||||
|
background-color: var(--pngx-bg-darker) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.color-scheme-dark {
|
body.color-scheme-dark {
|
||||||
|
@ -4,6 +4,7 @@ from guardian.admin import GuardedModelAdmin
|
|||||||
from .models import Correspondent
|
from .models import Correspondent
|
||||||
from .models import Document
|
from .models import Document
|
||||||
from .models import DocumentType
|
from .models import DocumentType
|
||||||
|
from .models import Note
|
||||||
from .models import PaperlessTask
|
from .models import PaperlessTask
|
||||||
from .models import SavedView
|
from .models import SavedView
|
||||||
from .models import SavedViewFilterRule
|
from .models import SavedViewFilterRule
|
||||||
@ -131,6 +132,13 @@ class TaskAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotesAdmin(GuardedModelAdmin):
|
||||||
|
|
||||||
|
list_display = ("user", "created", "note", "document")
|
||||||
|
list_filter = ("created", "user")
|
||||||
|
list_display_links = ("created",)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Correspondent, CorrespondentAdmin)
|
admin.site.register(Correspondent, CorrespondentAdmin)
|
||||||
admin.site.register(Tag, TagAdmin)
|
admin.site.register(Tag, TagAdmin)
|
||||||
admin.site.register(DocumentType, DocumentTypeAdmin)
|
admin.site.register(DocumentType, DocumentTypeAdmin)
|
||||||
@ -138,3 +146,4 @@ admin.site.register(Document, DocumentAdmin)
|
|||||||
admin.site.register(SavedView, SavedViewAdmin)
|
admin.site.register(SavedView, SavedViewAdmin)
|
||||||
admin.site.register(StoragePath, StoragePathAdmin)
|
admin.site.register(StoragePath, StoragePathAdmin)
|
||||||
admin.site.register(PaperlessTask, TaskAdmin)
|
admin.site.register(PaperlessTask, TaskAdmin)
|
||||||
|
admin.site.register(Note, NotesAdmin)
|
||||||
|
@ -36,29 +36,30 @@ class DocumentTypeFilterSet(FilterSet):
|
|||||||
fields = {"name": CHAR_KWARGS}
|
fields = {"name": CHAR_KWARGS}
|
||||||
|
|
||||||
|
|
||||||
class TagsFilter(Filter):
|
class ObjectFilter(Filter):
|
||||||
def __init__(self, exclude=False, in_list=False):
|
def __init__(self, exclude=False, in_list=False, field_name=""):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.exclude = exclude
|
self.exclude = exclude
|
||||||
self.in_list = in_list
|
self.in_list = in_list
|
||||||
|
self.field_name = field_name
|
||||||
|
|
||||||
def filter(self, qs, value):
|
def filter(self, qs, value):
|
||||||
if not value:
|
if not value:
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tag_ids = [int(x) for x in value.split(",")]
|
object_ids = [int(x) for x in value.split(",")]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
if self.in_list:
|
if self.in_list:
|
||||||
qs = qs.filter(tags__id__in=tag_ids).distinct()
|
qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
|
||||||
else:
|
else:
|
||||||
for tag_id in tag_ids:
|
for obj_id in object_ids:
|
||||||
if self.exclude:
|
if self.exclude:
|
||||||
qs = qs.exclude(tags__id=tag_id)
|
qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
|
||||||
else:
|
else:
|
||||||
qs = qs.filter(tags__id=tag_id)
|
qs = qs.filter(**{f"{self.field_name}__id": obj_id})
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@ -90,11 +91,17 @@ class DocumentFilterSet(FilterSet):
|
|||||||
exclude=True,
|
exclude=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
tags__id__all = TagsFilter()
|
tags__id__all = ObjectFilter(field_name="tags")
|
||||||
|
|
||||||
tags__id__none = TagsFilter(exclude=True)
|
tags__id__none = ObjectFilter(field_name="tags", exclude=True)
|
||||||
|
|
||||||
tags__id__in = TagsFilter(in_list=True)
|
tags__id__in = ObjectFilter(field_name="tags", in_list=True)
|
||||||
|
|
||||||
|
correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
|
||||||
|
|
||||||
|
document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
|
||||||
|
|
||||||
|
storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
|
||||||
|
|
||||||
is_in_inbox = InboxFilter()
|
is_in_inbox = InboxFilter()
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ from contextlib import contextmanager
|
|||||||
from dateutil.parser import isoparse
|
from dateutil.parser import isoparse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from documents.models import Comment
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from documents.models import Note
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
from whoosh import classify
|
from whoosh import classify
|
||||||
from whoosh import highlight
|
from whoosh import highlight
|
||||||
@ -52,7 +52,7 @@ def get_schema():
|
|||||||
path=TEXT(sortable=True),
|
path=TEXT(sortable=True),
|
||||||
path_id=NUMERIC(),
|
path_id=NUMERIC(),
|
||||||
has_path=BOOLEAN(),
|
has_path=BOOLEAN(),
|
||||||
comments=TEXT(),
|
notes=TEXT(),
|
||||||
owner=TEXT(),
|
owner=TEXT(),
|
||||||
owner_id=NUMERIC(),
|
owner_id=NUMERIC(),
|
||||||
has_owner=BOOLEAN(),
|
has_owner=BOOLEAN(),
|
||||||
@ -98,7 +98,7 @@ def open_index_searcher():
|
|||||||
def update_document(writer: AsyncWriter, doc: Document):
|
def update_document(writer: AsyncWriter, doc: Document):
|
||||||
tags = ",".join([t.name for t in doc.tags.all()])
|
tags = ",".join([t.name for t in doc.tags.all()])
|
||||||
tags_ids = ",".join([str(t.id) for t in doc.tags.all()])
|
tags_ids = ",".join([str(t.id) for t in doc.tags.all()])
|
||||||
comments = ",".join([str(c.comment) for c in Comment.objects.filter(document=doc)])
|
notes = ",".join([str(c.note) for c in Note.objects.filter(document=doc)])
|
||||||
asn = doc.archive_serial_number
|
asn = doc.archive_serial_number
|
||||||
if asn is not None and (
|
if asn is not None and (
|
||||||
asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||||
@ -136,7 +136,7 @@ def update_document(writer: AsyncWriter, doc: Document):
|
|||||||
path=doc.storage_path.name if doc.storage_path else None,
|
path=doc.storage_path.name if doc.storage_path else None,
|
||||||
path_id=doc.storage_path.id if doc.storage_path else None,
|
path_id=doc.storage_path.id if doc.storage_path else None,
|
||||||
has_path=doc.storage_path is not None,
|
has_path=doc.storage_path is not None,
|
||||||
comments=comments,
|
notes=notes,
|
||||||
owner=doc.owner.username if doc.owner else None,
|
owner=doc.owner.username if doc.owner else None,
|
||||||
owner_id=doc.owner.id if doc.owner else None,
|
owner_id=doc.owner.id if doc.owner else None,
|
||||||
has_owner=doc.owner is not None,
|
has_owner=doc.owner is not None,
|
||||||
@ -293,7 +293,7 @@ class DelayedFullTextQuery(DelayedQuery):
|
|||||||
def _get_query(self):
|
def _get_query(self):
|
||||||
q_str = self.query_params["query"]
|
q_str = self.query_params["query"]
|
||||||
qp = MultifieldParser(
|
qp = MultifieldParser(
|
||||||
["content", "title", "correspondent", "tag", "type", "comments"],
|
["content", "title", "correspondent", "tag", "type", "notes"],
|
||||||
self.searcher.ixreader.schema,
|
self.searcher.ixreader.schema,
|
||||||
)
|
)
|
||||||
qp.add_plugin(DateParserPlugin(basedate=timezone.now()))
|
qp.add_plugin(DateParserPlugin(basedate=timezone.now()))
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from fnmatch import filter
|
from fnmatch import filter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from threading import Thread
|
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Final
|
from typing import Final
|
||||||
@ -168,11 +168,15 @@ def _consume_wait_unmodified(file: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
class Handler(FileSystemEventHandler):
|
class Handler(FileSystemEventHandler):
|
||||||
|
def __init__(self, pool: ThreadPoolExecutor) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._pool = pool
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
Thread(target=_consume_wait_unmodified, args=(event.src_path,)).start()
|
self._pool.submit(_consume_wait_unmodified, event.src_path)
|
||||||
|
|
||||||
def on_moved(self, event):
|
def on_moved(self, event):
|
||||||
Thread(target=_consume_wait_unmodified, args=(event.dest_path,)).start()
|
self._pool.submit(_consume_wait_unmodified, event.dest_path)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -246,17 +250,18 @@ class Command(BaseCommand):
|
|||||||
timeout = self.testing_timeout_s
|
timeout = self.testing_timeout_s
|
||||||
logger.debug(f"Configuring timeout to {timeout}s")
|
logger.debug(f"Configuring timeout to {timeout}s")
|
||||||
|
|
||||||
observer = PollingObserver(timeout=settings.CONSUMER_POLLING)
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||||
observer.schedule(Handler(), directory, recursive=recursive)
|
observer = PollingObserver(timeout=settings.CONSUMER_POLLING)
|
||||||
observer.start()
|
observer.schedule(Handler(pool), directory, recursive=recursive)
|
||||||
try:
|
observer.start()
|
||||||
while observer.is_alive():
|
try:
|
||||||
observer.join(timeout)
|
while observer.is_alive():
|
||||||
if self.stop_flag.is_set():
|
observer.join(timeout)
|
||||||
observer.stop()
|
if self.stop_flag.is_set():
|
||||||
except KeyboardInterrupt:
|
observer.stop()
|
||||||
observer.stop()
|
except KeyboardInterrupt:
|
||||||
observer.join()
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
|
||||||
def handle_inotify(self, directory, recursive, is_testing: bool):
|
def handle_inotify(self, directory, recursive, is_testing: bool):
|
||||||
logger.info(f"Using inotify to watch directory for changes: {directory}")
|
logger.info(f"Using inotify to watch directory for changes: {directory}")
|
||||||
|
@ -17,10 +17,10 @@ from django.core.management.base import BaseCommand
|
|||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from documents.models import Comment
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
|
from documents.models import Note
|
||||||
from documents.models import SavedView
|
from documents.models import SavedView
|
||||||
from documents.models import SavedViewFilterRule
|
from documents.models import SavedViewFilterRule
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
@ -206,7 +206,7 @@ class Command(BaseCommand):
|
|||||||
self.files_in_export_dir.add(x.resolve())
|
self.files_in_export_dir.add(x.resolve())
|
||||||
|
|
||||||
# 2. Create manifest, containing all correspondents, types, tags, storage paths
|
# 2. Create manifest, containing all correspondents, types, tags, storage paths
|
||||||
# comments, documents and ui_settings
|
# note, documents and ui_settings
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
manifest = json.loads(
|
manifest = json.loads(
|
||||||
serializers.serialize("json", Correspondent.objects.all()),
|
serializers.serialize("json", Correspondent.objects.all()),
|
||||||
@ -222,11 +222,11 @@ class Command(BaseCommand):
|
|||||||
serializers.serialize("json", StoragePath.objects.all()),
|
serializers.serialize("json", StoragePath.objects.all()),
|
||||||
)
|
)
|
||||||
|
|
||||||
comments = json.loads(
|
notes = json.loads(
|
||||||
serializers.serialize("json", Comment.objects.all()),
|
serializers.serialize("json", Note.objects.all()),
|
||||||
)
|
)
|
||||||
if not self.split_manifest:
|
if not self.split_manifest:
|
||||||
manifest += comments
|
manifest += notes
|
||||||
|
|
||||||
documents = Document.objects.order_by("id")
|
documents = Document.objects.order_by("id")
|
||||||
document_map = {d.pk: d for d in documents}
|
document_map = {d.pk: d for d in documents}
|
||||||
@ -359,7 +359,7 @@ class Command(BaseCommand):
|
|||||||
content += list(
|
content += list(
|
||||||
filter(
|
filter(
|
||||||
lambda d: d["fields"]["document"] == document_dict["pk"],
|
lambda d: d["fields"]["document"] == document_dict["pk"],
|
||||||
comments,
|
notes,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
manifest_name.write_text(json.dumps(content, indent=2))
|
manifest_name.write_text(json.dumps(content, indent=2))
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-15 07:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="savedviewfilterrule",
|
||||||
|
name="rule_type",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, "title contains"),
|
||||||
|
(1, "content contains"),
|
||||||
|
(2, "ASN is"),
|
||||||
|
(3, "correspondent is"),
|
||||||
|
(4, "document type is"),
|
||||||
|
(5, "is in inbox"),
|
||||||
|
(6, "has tag"),
|
||||||
|
(7, "has any tag"),
|
||||||
|
(8, "created before"),
|
||||||
|
(9, "created after"),
|
||||||
|
(10, "created year is"),
|
||||||
|
(11, "created month is"),
|
||||||
|
(12, "created day is"),
|
||||||
|
(13, "added before"),
|
||||||
|
(14, "added after"),
|
||||||
|
(15, "modified before"),
|
||||||
|
(16, "modified after"),
|
||||||
|
(17, "does not have tag"),
|
||||||
|
(18, "does not have ASN"),
|
||||||
|
(19, "title or content contains"),
|
||||||
|
(20, "fulltext query"),
|
||||||
|
(21, "more like this"),
|
||||||
|
(22, "has tags in"),
|
||||||
|
(23, "ASN greater than"),
|
||||||
|
(24, "ASN less than"),
|
||||||
|
(25, "storage path is"),
|
||||||
|
(26, "has correspondent in"),
|
||||||
|
(27, "does not have correspondent in"),
|
||||||
|
(28, "has document type in"),
|
||||||
|
(29, "does not have document type in"),
|
||||||
|
(30, "has storage path in"),
|
||||||
|
(31, "does not have storage path in"),
|
||||||
|
],
|
||||||
|
verbose_name="rule type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
61
src/documents/migrations/1035_rename_comment_note.py
Normal file
61
src/documents/migrations/1035_rename_comment_note.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-17 22:15
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("documents", "1034_alter_savedviewfilterrule_rule_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name="Comment",
|
||||||
|
new_name="Note",
|
||||||
|
),
|
||||||
|
migrations.RenameField(model_name="note", old_name="comment", new_name="note"),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="note",
|
||||||
|
options={
|
||||||
|
"ordering": ("created",),
|
||||||
|
"verbose_name": "note",
|
||||||
|
"verbose_name_plural": "notes",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="document",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="notes",
|
||||||
|
to="documents.document",
|
||||||
|
verbose_name="document",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="note",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True, help_text="Note for the document", verbose_name="content"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="note",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="notes",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -447,6 +447,12 @@ class SavedViewFilterRule(models.Model):
|
|||||||
(23, _("ASN greater than")),
|
(23, _("ASN greater than")),
|
||||||
(24, _("ASN less than")),
|
(24, _("ASN less than")),
|
||||||
(25, _("storage path is")),
|
(25, _("storage path is")),
|
||||||
|
(26, _("has correspondent in")),
|
||||||
|
(27, _("does not have correspondent in")),
|
||||||
|
(28, _("has document type in")),
|
||||||
|
(29, _("does not have document type in")),
|
||||||
|
(30, _("has storage path in")),
|
||||||
|
(31, _("does not have storage path in")),
|
||||||
]
|
]
|
||||||
|
|
||||||
saved_view = models.ForeignKey(
|
saved_view = models.ForeignKey(
|
||||||
@ -629,11 +635,11 @@ class PaperlessTask(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Note(models.Model):
|
||||||
comment = models.TextField(
|
note = models.TextField(
|
||||||
_("content"),
|
_("content"),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("Comment for the document"),
|
help_text=_("Note for the document"),
|
||||||
)
|
)
|
||||||
|
|
||||||
created = models.DateTimeField(
|
created = models.DateTimeField(
|
||||||
@ -646,7 +652,7 @@ class Comment(models.Model):
|
|||||||
Document,
|
Document,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="documents",
|
related_name="notes",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_("document"),
|
verbose_name=_("document"),
|
||||||
)
|
)
|
||||||
@ -655,15 +661,15 @@ class Comment(models.Model):
|
|||||||
User,
|
User,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="users",
|
related_name="notes",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
verbose_name=_("user"),
|
verbose_name=_("user"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("created",)
|
ordering = ("created",)
|
||||||
verbose_name = _("comment")
|
verbose_name = _("note")
|
||||||
verbose_name_plural = _("comments")
|
verbose_name_plural = _("notes")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.content
|
return self.note
|
||||||
|
@ -78,10 +78,11 @@ class MatchingModelSerializer(serializers.ModelSerializer):
|
|||||||
if hasattr(self, "user")
|
if hasattr(self, "user")
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
pk = self.instance.pk if hasattr(self.instance, "pk") else None
|
||||||
if ("name" in data or "owner" in data) and self.Meta.model.objects.filter(
|
if ("name" in data or "owner" in data) and self.Meta.model.objects.filter(
|
||||||
name=name,
|
name=name,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
).exists():
|
).exclude(pk=pk).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "Object violates owner / name unique constraint"},
|
{"error": "Object violates owner / name unique constraint"},
|
||||||
)
|
)
|
||||||
@ -442,6 +443,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer):
|
|||||||
"owner",
|
"owner",
|
||||||
"permissions",
|
"permissions",
|
||||||
"set_permissions",
|
"set_permissions",
|
||||||
|
"notes",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ from documents.models import PaperlessTask
|
|||||||
from documents.models import SavedView
|
from documents.models import SavedView
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import Comment
|
from documents.models import Note
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
from paperless import version
|
from paperless import version
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
@ -1039,9 +1039,24 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
def test_statistics(self):
|
def test_statistics(self):
|
||||||
|
|
||||||
doc1 = Document.objects.create(title="none1", checksum="A")
|
doc1 = Document.objects.create(
|
||||||
doc2 = Document.objects.create(title="none2", checksum="B")
|
title="none1",
|
||||||
doc3 = Document.objects.create(title="none3", checksum="C")
|
checksum="A",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
content="abc",
|
||||||
|
)
|
||||||
|
doc2 = Document.objects.create(
|
||||||
|
title="none2",
|
||||||
|
checksum="B",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
content="123",
|
||||||
|
)
|
||||||
|
doc3 = Document.objects.create(
|
||||||
|
title="none3",
|
||||||
|
checksum="C",
|
||||||
|
mime_type="text/plain",
|
||||||
|
content="hello",
|
||||||
|
)
|
||||||
|
|
||||||
tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
|
tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
|
||||||
|
|
||||||
@ -1051,6 +1066,16 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["documents_total"], 3)
|
self.assertEqual(response.data["documents_total"], 3)
|
||||||
self.assertEqual(response.data["documents_inbox"], 1)
|
self.assertEqual(response.data["documents_inbox"], 1)
|
||||||
|
self.assertEqual(response.data["inbox_tag"], tag_inbox.pk)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["document_file_type_counts"][0]["mime_type_count"],
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["document_file_type_counts"][1]["mime_type_count"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.data["character_count"], 11)
|
||||||
|
|
||||||
def test_statistics_no_inbox_tag(self):
|
def test_statistics_no_inbox_tag(self):
|
||||||
Document.objects.create(title="none1", checksum="A")
|
Document.objects.create(title="none1", checksum="A")
|
||||||
@ -1058,6 +1083,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
response = self.client.get("/api/statistics/")
|
response = self.client.get("/api/statistics/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["documents_inbox"], None)
|
self.assertEqual(response.data["documents_inbox"], None)
|
||||||
|
self.assertEqual(response.data["inbox_tag"], None)
|
||||||
|
|
||||||
@mock.patch("documents.views.consume_file.delay")
|
@mock.patch("documents.views.consume_file.delay")
|
||||||
def test_upload(self, m):
|
def test_upload(self, m):
|
||||||
@ -1717,28 +1743,28 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_existing_comments(self):
|
def test_get_existing_notes(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- A document with a single comment
|
- A document with a single note
|
||||||
WHEN:
|
WHEN:
|
||||||
- API reuqest for document comments is made
|
- API reuqest for document notes is made
|
||||||
THEN:
|
THEN:
|
||||||
- The associated comment is returned
|
- The associated note is returned
|
||||||
"""
|
"""
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="test",
|
title="test",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
content="this is a document which will have comments!",
|
content="this is a document which will have notes!",
|
||||||
)
|
)
|
||||||
comment = Comment.objects.create(
|
note = Note.objects.create(
|
||||||
comment="This is a comment.",
|
note="This is a note.",
|
||||||
document=doc,
|
document=doc,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
f"/api/documents/{doc.pk}/comments/",
|
f"/api/documents/{doc.pk}/notes/",
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1754,39 +1780,39 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
resp_data,
|
resp_data,
|
||||||
{
|
{
|
||||||
"id": comment.id,
|
"id": note.id,
|
||||||
"comment": comment.comment,
|
"note": note.note,
|
||||||
"user": {
|
"user": {
|
||||||
"id": comment.user.id,
|
"id": note.user.id,
|
||||||
"username": comment.user.username,
|
"username": note.user.username,
|
||||||
"first_name": comment.user.first_name,
|
"first_name": note.user.first_name,
|
||||||
"last_name": comment.user.last_name,
|
"last_name": note.user.last_name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_comment(self):
|
def test_create_note(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing document
|
- Existing document
|
||||||
WHEN:
|
WHEN:
|
||||||
- API request is made to add a comment
|
- API request is made to add a note
|
||||||
THEN:
|
THEN:
|
||||||
- Comment is created and associated with document
|
- note is created and associated with document
|
||||||
"""
|
"""
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="test",
|
title="test",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
content="this is a document which will have comments added",
|
content="this is a document which will have notes added",
|
||||||
)
|
)
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
f"/api/documents/{doc.pk}/comments/",
|
f"/api/documents/{doc.pk}/notes/",
|
||||||
data={"comment": "this is a posted comment"},
|
data={"note": "this is a posted note"},
|
||||||
)
|
)
|
||||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
f"/api/documents/{doc.pk}/comments/",
|
f"/api/documents/{doc.pk}/notes/",
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1798,48 +1824,48 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
resp_data = resp_data[0]
|
resp_data = resp_data[0]
|
||||||
|
|
||||||
self.assertEqual(resp_data["comment"], "this is a posted comment")
|
self.assertEqual(resp_data["note"], "this is a posted note")
|
||||||
|
|
||||||
def test_delete_comment(self):
|
def test_delete_note(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing document
|
- Existing document
|
||||||
WHEN:
|
WHEN:
|
||||||
- API request is made to add a comment
|
- API request is made to add a note
|
||||||
THEN:
|
THEN:
|
||||||
- Comment is created and associated with document
|
- note is created and associated with document
|
||||||
"""
|
"""
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="test",
|
title="test",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
content="this is a document which will have comments!",
|
content="this is a document which will have notes!",
|
||||||
)
|
)
|
||||||
comment = Comment.objects.create(
|
note = Note.objects.create(
|
||||||
comment="This is a comment.",
|
note="This is a note.",
|
||||||
document=doc,
|
document=doc,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
f"/api/documents/{doc.pk}/comments/?id={comment.pk}",
|
f"/api/documents/{doc.pk}/notes/?id={note.pk}",
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
self.assertEqual(len(Comment.objects.all()), 0)
|
self.assertEqual(len(Note.objects.all()), 0)
|
||||||
|
|
||||||
def test_get_comments_no_doc(self):
|
def test_get_notes_no_doc(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- A request to get comments from a non-existent document
|
- A request to get notes from a non-existent document
|
||||||
WHEN:
|
WHEN:
|
||||||
- API request for document comments is made
|
- API request for document notes is made
|
||||||
THEN:
|
THEN:
|
||||||
- HTTP status.HTTP_404_NOT_FOUND is returned
|
- HTTP status.HTTP_404_NOT_FOUND is returned
|
||||||
"""
|
"""
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/api/documents/500/comments/",
|
"/api/documents/500/notes/",
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
@ -13,10 +13,10 @@ from django.test import override_settings
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from documents.management.commands import document_exporter
|
from documents.management.commands import document_exporter
|
||||||
from documents.models import Comment
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
|
from documents.models import Note
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import User
|
from documents.models import User
|
||||||
@ -66,8 +66,8 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
storage_type=Document.STORAGE_TYPE_GPG,
|
storage_type=Document.STORAGE_TYPE_GPG,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.comment = Comment.objects.create(
|
self.note = Note.objects.create(
|
||||||
comment="This is a comment. amaze.",
|
note="This is a note. amaze.",
|
||||||
document=self.d1,
|
document=self.d1,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
@ -199,8 +199,8 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
self.assertEqual(checksum, element["fields"]["archive_checksum"])
|
self.assertEqual(checksum, element["fields"]["archive_checksum"])
|
||||||
|
|
||||||
elif element["model"] == "documents.comment":
|
elif element["model"] == "documents.note":
|
||||||
self.assertEqual(element["fields"]["comment"], self.comment.comment)
|
self.assertEqual(element["fields"]["note"], self.note.note)
|
||||||
self.assertEqual(element["fields"]["document"], self.d1.id)
|
self.assertEqual(element["fields"]["document"], self.d1.id)
|
||||||
self.assertEqual(element["fields"]["user"], self.user.id)
|
self.assertEqual(element["fields"]["user"], self.user.id)
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@ from django.db.models import Case
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.db.models import IntegerField
|
from django.db.models import IntegerField
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
|
from django.db.models import Sum
|
||||||
from django.db.models import When
|
from django.db.models import When
|
||||||
|
from django.db.models.functions import Length
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
@ -72,10 +74,10 @@ from .matching import match_correspondents
|
|||||||
from .matching import match_document_types
|
from .matching import match_document_types
|
||||||
from .matching import match_storage_paths
|
from .matching import match_storage_paths
|
||||||
from .matching import match_tags
|
from .matching import match_tags
|
||||||
from .models import Comment
|
|
||||||
from .models import Correspondent
|
from .models import Correspondent
|
||||||
from .models import Document
|
from .models import Document
|
||||||
from .models import DocumentType
|
from .models import DocumentType
|
||||||
|
from .models import Note
|
||||||
from .models import PaperlessTask
|
from .models import PaperlessTask
|
||||||
from .models import SavedView
|
from .models import SavedView
|
||||||
from .models import StoragePath
|
from .models import StoragePath
|
||||||
@ -186,6 +188,7 @@ class TagViewSet(ModelViewSet, PassUserMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
def get_serializer_class(self, *args, **kwargs):
|
||||||
|
print(self.request.version)
|
||||||
if int(self.request.version) == 1:
|
if int(self.request.version) == 1:
|
||||||
return TagSerializerVersion1
|
return TagSerializerVersion1
|
||||||
else:
|
else:
|
||||||
@ -230,7 +233,7 @@ class DocumentViewSet(
|
|||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
model = Document
|
model = Document
|
||||||
queryset = Document.objects.all()
|
queryset = Document.objects.annotate(num_notes=Count("notes"))
|
||||||
serializer_class = DocumentSerializer
|
serializer_class = DocumentSerializer
|
||||||
pagination_class = StandardPagination
|
pagination_class = StandardPagination
|
||||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
@ -251,10 +254,11 @@ class DocumentViewSet(
|
|||||||
"modified",
|
"modified",
|
||||||
"added",
|
"added",
|
||||||
"archive_serial_number",
|
"archive_serial_number",
|
||||||
|
"num_notes",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Document.objects.distinct()
|
return Document.objects.distinct().annotate(num_notes=Count("notes"))
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
super().get_serializer(*args, **kwargs)
|
super().get_serializer(*args, **kwargs)
|
||||||
@ -441,11 +445,11 @@ class DocumentViewSet(
|
|||||||
except (FileNotFoundError, Document.DoesNotExist):
|
except (FileNotFoundError, Document.DoesNotExist):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
def getComments(self, doc):
|
def getNotes(self, doc):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"id": c.id,
|
"id": c.id,
|
||||||
"comment": c.comment,
|
"note": c.note,
|
||||||
"created": c.created,
|
"created": c.created,
|
||||||
"user": {
|
"user": {
|
||||||
"id": c.user.id,
|
"id": c.user.id,
|
||||||
@ -454,11 +458,11 @@ class DocumentViewSet(
|
|||||||
"last_name": c.user.last_name,
|
"last_name": c.user.last_name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for c in Comment.objects.filter(document=doc).order_by("-created")
|
for c in Note.objects.filter(document=doc).order_by("-created")
|
||||||
]
|
]
|
||||||
|
|
||||||
@action(methods=["get", "post", "delete"], detail=True)
|
@action(methods=["get", "post", "delete"], detail=True)
|
||||||
def comments(self, request, pk=None):
|
def notes(self, request, pk=None):
|
||||||
try:
|
try:
|
||||||
doc = Document.objects.get(pk=pk)
|
doc = Document.objects.get(pk=pk)
|
||||||
except Document.DoesNotExist:
|
except Document.DoesNotExist:
|
||||||
@ -468,17 +472,17 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
try:
|
try:
|
||||||
return Response(self.getComments(doc))
|
return Response(self.getNotes(doc))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"An error occurred retrieving comments: {str(e)}")
|
logger.warning(f"An error occurred retrieving notes: {str(e)}")
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Error retreiving comments, check logs for more detail."},
|
{"error": "Error retreiving notes, check logs for more detail."},
|
||||||
)
|
)
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
try:
|
try:
|
||||||
c = Comment.objects.create(
|
c = Note.objects.create(
|
||||||
document=doc,
|
document=doc,
|
||||||
comment=request.data["comment"],
|
note=request.data["note"],
|
||||||
user=currentUser,
|
user=currentUser,
|
||||||
)
|
)
|
||||||
c.save()
|
c.save()
|
||||||
@ -487,23 +491,23 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
index.add_or_update_document(self.get_object())
|
index.add_or_update_document(self.get_object())
|
||||||
|
|
||||||
return Response(self.getComments(doc))
|
return Response(self.getNotes(doc))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"An error occurred saving comment: {str(e)}")
|
logger.warning(f"An error occurred saving note: {str(e)}")
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Error saving comment, check logs for more detail.",
|
"error": "Error saving note, check logs for more detail.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
elif request.method == "DELETE":
|
elif request.method == "DELETE":
|
||||||
comment = Comment.objects.get(id=int(request.GET.get("id")))
|
note = Note.objects.get(id=int(request.GET.get("id")))
|
||||||
comment.delete()
|
note.delete()
|
||||||
|
|
||||||
from documents import index
|
from documents import index
|
||||||
|
|
||||||
index.add_or_update_document(self.get_object())
|
index.add_or_update_document(self.get_object())
|
||||||
|
|
||||||
return Response(self.getComments(doc))
|
return Response(self.getNotes(doc))
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -515,14 +519,14 @@ class DocumentViewSet(
|
|||||||
class SearchResultSerializer(DocumentSerializer, PassUserMixin):
|
class SearchResultSerializer(DocumentSerializer, PassUserMixin):
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
doc = Document.objects.get(id=instance["id"])
|
doc = Document.objects.get(id=instance["id"])
|
||||||
comments = ",".join(
|
notes = ",".join(
|
||||||
[str(c.comment) for c in Comment.objects.filter(document=instance["id"])],
|
[str(c.note) for c in Note.objects.filter(document=instance["id"])],
|
||||||
)
|
)
|
||||||
r = super().to_representation(doc)
|
r = super().to_representation(doc)
|
||||||
r["__search_hit__"] = {
|
r["__search_hit__"] = {
|
||||||
"score": instance.score,
|
"score": instance.score,
|
||||||
"highlights": instance.highlights("content", text=doc.content),
|
"highlights": instance.highlights("content", text=doc.content),
|
||||||
"comment_highlights": instance.highlights("comments", text=comments)
|
"note_highlights": instance.highlights("notes", text=notes)
|
||||||
if doc
|
if doc
|
||||||
else None,
|
else None,
|
||||||
"rank": instance.rank,
|
"rank": instance.rank,
|
||||||
@ -794,17 +798,38 @@ class StatisticsView(APIView):
|
|||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
documents_total = Document.objects.all().count()
|
documents_total = Document.objects.all().count()
|
||||||
if Tag.objects.filter(is_inbox_tag=True).exists():
|
|
||||||
documents_inbox = (
|
inbox_tag = Tag.objects.filter(is_inbox_tag=True)
|
||||||
Document.objects.filter(tags__is_inbox_tag=True).distinct().count()
|
|
||||||
|
documents_inbox = (
|
||||||
|
Document.objects.filter(tags__is_inbox_tag=True).distinct().count()
|
||||||
|
if inbox_tag.exists()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
document_file_type_counts = (
|
||||||
|
Document.objects.values("mime_type")
|
||||||
|
.annotate(mime_type_count=Count("mime_type"))
|
||||||
|
.order_by("-mime_type_count")
|
||||||
|
if documents_total > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
character_count = (
|
||||||
|
Document.objects.annotate(
|
||||||
|
characters=Length("content"),
|
||||||
)
|
)
|
||||||
else:
|
.aggregate(Sum("characters"))
|
||||||
documents_inbox = None
|
.get("characters__sum")
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"documents_total": documents_total,
|
"documents_total": documents_total,
|
||||||
"documents_inbox": documents_inbox,
|
"documents_inbox": documents_inbox,
|
||||||
|
"inbox_tag": inbox_tag.first().pk if inbox_tag.exists() else None,
|
||||||
|
"document_file_type_counts": document_file_type_counts,
|
||||||
|
"character_count": character_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -358,7 +358,7 @@ TEMPLATES = [
|
|||||||
|
|
||||||
CHANNEL_LAYERS = {
|
CHANNEL_LAYERS = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
"BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
|
||||||
"CONFIG": {
|
"CONFIG": {
|
||||||
"hosts": [_CHANNELS_REDIS_URL],
|
"hosts": [_CHANNELS_REDIS_URL],
|
||||||
"capacity": 2000, # default 100
|
"capacity": 2000, # default 100
|
||||||
@ -509,7 +509,12 @@ if os.getenv("PAPERLESS_DBHOST"):
|
|||||||
|
|
||||||
else: # Default to PostgresDB
|
else: # Default to PostgresDB
|
||||||
engine = "django.db.backends.postgresql_psycopg2"
|
engine = "django.db.backends.postgresql_psycopg2"
|
||||||
options = {"sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer")}
|
options = {
|
||||||
|
"sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer"),
|
||||||
|
"sslrootcert": os.getenv("PAPERLESS_DBSSLROOTCERT", None),
|
||||||
|
"sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
|
||||||
|
"sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
|
||||||
|
}
|
||||||
|
|
||||||
DATABASES["default"]["ENGINE"] = engine
|
DATABASES["default"]["ENGINE"] = engine
|
||||||
DATABASES["default"]["OPTIONS"].update(options)
|
DATABASES["default"]["OPTIONS"].update(options)
|
||||||
@ -606,11 +611,20 @@ LOGGING = {
|
|||||||
"maxBytes": LOGROTATE_MAX_SIZE,
|
"maxBytes": LOGROTATE_MAX_SIZE,
|
||||||
"backupCount": LOGROTATE_MAX_BACKUPS,
|
"backupCount": LOGROTATE_MAX_BACKUPS,
|
||||||
},
|
},
|
||||||
|
"file_celery": {
|
||||||
|
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
|
||||||
|
"formatter": "verbose",
|
||||||
|
"filename": os.path.join(LOGGING_DIR, "celery.log"),
|
||||||
|
"maxBytes": LOGROTATE_MAX_SIZE,
|
||||||
|
"backupCount": LOGROTATE_MAX_BACKUPS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"root": {"handlers": ["console"]},
|
"root": {"handlers": ["console"]},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
"paperless": {"handlers": ["file_paperless"], "level": "DEBUG"},
|
"paperless": {"handlers": ["file_paperless"], "level": "DEBUG"},
|
||||||
"paperless_mail": {"handlers": ["file_mail"], "level": "DEBUG"},
|
"paperless_mail": {"handlers": ["file_mail"], "level": "DEBUG"},
|
||||||
|
"celery": {"handlers": ["file_celery"], "level": "DEBUG"},
|
||||||
|
"kombu": {"handlers": ["file_celery"], "level": "DEBUG"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ from paperless.consumers import StatusConsumer
|
|||||||
from paperless.views import FaviconView
|
from paperless.views import FaviconView
|
||||||
from paperless.views import GroupViewSet
|
from paperless.views import GroupViewSet
|
||||||
from paperless.views import UserViewSet
|
from paperless.views import UserViewSet
|
||||||
|
from paperless_mail.views import MailAccountTestView
|
||||||
from paperless_mail.views import MailAccountViewSet
|
from paperless_mail.views import MailAccountViewSet
|
||||||
from paperless_mail.views import MailRuleViewSet
|
from paperless_mail.views import MailRuleViewSet
|
||||||
from rest_framework.authtoken import views
|
from rest_framework.authtoken import views
|
||||||
@ -102,6 +103,11 @@ urlpatterns = [
|
|||||||
AcknowledgeTasksView.as_view(),
|
AcknowledgeTasksView.as_view(),
|
||||||
name="acknowledge_tasks",
|
name="acknowledge_tasks",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^mail_accounts/test/",
|
||||||
|
MailAccountTestView.as_view(),
|
||||||
|
name="mail_accounts_test",
|
||||||
|
),
|
||||||
path("token/", views.obtain_auth_token),
|
path("token/", views.obtain_auth_token),
|
||||||
]
|
]
|
||||||
+ api_router.urls,
|
+ api_router.urls,
|
||||||
|
@ -202,20 +202,21 @@ def mailbox_login(mailbox: MailBox, account: MailAccount):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
mailbox.login(account.username, account.password)
|
if account.is_token:
|
||||||
|
mailbox.xoauth2(account.username, account.password)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
_ = account.password.encode("ascii")
|
||||||
|
use_ascii_login = True
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
use_ascii_login = False
|
||||||
|
|
||||||
except UnicodeEncodeError:
|
if use_ascii_login:
|
||||||
logger.debug("Falling back to AUTH=PLAIN")
|
mailbox.login(account.username, account.password)
|
||||||
|
else:
|
||||||
|
logger.debug("Falling back to AUTH=PLAIN")
|
||||||
|
mailbox.login_utf8(account.username, account.password)
|
||||||
|
|
||||||
try:
|
|
||||||
mailbox.login_utf8(account.username, account.password)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
"Unable to authenticate with mail server using AUTH=PLAIN",
|
|
||||||
)
|
|
||||||
raise MailError(
|
|
||||||
f"Error while authenticating account {account}",
|
|
||||||
) from e
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error while authenticating account {account}: {e}",
|
f"Error while authenticating account {account}: {e}",
|
||||||
|
19
src/paperless_mail/migrations/0020_mailaccount_is_token.py
Normal file
19
src/paperless_mail/migrations/0020_mailaccount_is_token.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-22 17:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("paperless_mail", "0019_mailrule_filter_to"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="mailaccount",
|
||||||
|
name="is_token",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name="Is token authentication"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -38,6 +38,8 @@ class MailAccount(document_models.ModelWithOwner):
|
|||||||
|
|
||||||
password = models.CharField(_("password"), max_length=256)
|
password = models.CharField(_("password"), max_length=256)
|
||||||
|
|
||||||
|
is_token = models.BooleanField(_("Is token authentication"), default=False)
|
||||||
|
|
||||||
character_set = models.CharField(
|
character_set = models.CharField(
|
||||||
_("character set"),
|
_("character set"),
|
||||||
max_length=256,
|
max_length=256,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user