Compare commits

..

1 Commits

Author SHA1 Message Date
shamoon
026daac77d 2.0.0-beta.1 2023-11-13 19:59:49 -08:00
248 changed files with 22605 additions and 45594 deletions

1
.env
View File

@@ -1 +1,2 @@
COMPOSE_PROJECT_NAME=paperless
export PROMPT="(pipenv-projectname)$P$G"

View File

@@ -45,7 +45,7 @@ jobs:
uses: pre-commit/action@v3.0.0
documentation:
name: "Build & Deploy Documentation"
name: "Build Documentation"
runs-on: ubuntu-22.04
needs:
- pre-commit
@@ -77,14 +77,6 @@ jobs:
name: Make documentation
run: |
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs build --config-file ./mkdocs.yml
-
name: Deploy documentation
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history
-
name: Upload artifact
uses: actions/upload-artifact@v3
@@ -93,6 +85,25 @@ jobs:
path: site/
retention-days: 7
documentation-deploy:
name: "Deploy Documentation"
runs-on: ubuntu-22.04
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- documentation
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: docs.paperless-ngx.com
CONFIG_FILE: mkdocs.yml
EXTRA_PACKAGES: build-base
tests-backend:
name: "Backend Tests (Python ${{ matrix.python-version }})"
runs-on: ubuntu-22.04
@@ -109,8 +120,8 @@ jobs:
-
name: Start containers
run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml pull --quiet
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml up --detach
-
name: Set up Python
id: setup-python
@@ -165,8 +176,8 @@ jobs:
name: Stop containers
if: always()
run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml logs
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml down
install-frontend-depedendencies:
name: "Install Frontend Dependendencies"
@@ -426,7 +437,6 @@ jobs:
name: "Build Release"
needs:
- build-docker-image
- documentation
runs-on: ubuntu-22.04
steps:
-
@@ -632,7 +642,7 @@ jobs:
git push origin ${{ needs.publish-release.outputs.version }}-changelog
-
name: Create Pull Request
uses: actions/github-script@v7
uses: actions/github-script@v6
with:
script: |
const { repo, owner } = context.repo;

View File

@@ -19,13 +19,9 @@ concurrency:
jobs:
cleanup-images:
name: Cleanup Image Tags for ${{ matrix.primary-name }}
name: Cleanup Image Tags for paperless-ngx
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
@@ -33,12 +29,12 @@ jobs:
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
is_org: "true"
package_name: "${{ matrix.primary-name }}"
package_name: "paperless-ngx"
scheme: "branch"
repo_name: "paperless-ngx"
match_regex: "feature-"
@@ -53,7 +49,18 @@ jobs:
strategy:
fail-fast: false
matrix:
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
include:
- primary-name: "paperless-ngx"
- primary-name: "paperless-ngx/builder/cache/app"
# TODO: Remove the above and replace with the below
# - primary-name: "builder/qpdf"
# - primary-name: "builder/cache/qpdf"
# - primary-name: "builder/pikepdf"
# - primary-name: "builder/cache/pikepdf"
# - primary-name: "builder/jbig2enc"
# - primary-name: "builder/cache/jbig2enc"
# - primary-name: "builder/psycopg2"
# - primary-name: "builder/cache/psycopg2"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
@@ -61,7 +68,7 @@ jobs:
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.4.0
uses: stumpylog/image-cleaner-action/untagged@v0.3.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"

View File

@@ -1,33 +0,0 @@
name: Crowdin Action
on:
workflow_dispatch:
schedule:
- cron: '2 */12 * * *'
push:
paths: [
'src/locale/**',
'src-ui/src/locale/**'
]
branches: [ dev ]
jobs:
synchronize-with-crowdin:
name: Crowdin Sync
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: crowdin action
uses: crowdin/github-action@v1
with:
upload_translations: false
download_translations: true
crowdin_branch_name: 'dev'
localization_branch_name: l10n_dev
pull_request_labels: 'skip-changelog, translation'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -1,6 +1,10 @@
name: Project Automations
on:
issues:
types:
- opened
- reopened
pull_request_target: #_target allows access to secrets
types:
- opened
@@ -12,7 +16,25 @@ on:
permissions:
contents: read
env:
todo: Todo
done: Done
in_progress: In Progress
jobs:
issue_opened_or_reopened:
name: issue_opened_or_reopened
runs-on: ubuntu-22.04
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
steps:
- name: Add issue to project and set status to ${{ env.todo }}
uses: leonsteinhaeuser/project-beta-automations@v2.2.1
with:
gh_token: ${{ secrets.GH_TOKEN }}
organization: paperless-ngx
project_id: 2
resource_node_id: ${{ github.event.issue.node_id }}
status_value: ${{ env.todo }} # Target status
pr_opened_or_reopened:
name: pr_opened_or_reopened
runs-on: ubuntu-22.04
@@ -21,6 +43,14 @@ jobs:
pull-requests: write
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps:
- name: Add PR to project and set status to "Needs Review"
uses: leonsteinhaeuser/project-beta-automations@v2.2.1
with:
gh_token: ${{ secrets.GH_TOKEN }}
organization: paperless-ngx
project_id: 2
resource_node_id: ${{ github.event.pull_request.node_id }}
status_value: "Needs Review" # Target status
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@v5
env:

View File

@@ -33,11 +33,10 @@ jobs:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
- uses: dessant/lock-threads@v4
with:
issue-inactive-days: '30'
pr-inactive-days: '30'
discussion-inactive-days: '30'
log-output: true
issue-comment: >
This issue has been automatically locked since there
@@ -47,21 +46,13 @@ jobs:
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns.
discussion-comment: >
This discussion has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion for related concerns.
close-answered-discussions:
name: 'Close Answered Discussions'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@v6
with:
script: |
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const query = `query($owner:String!, $name:String!) {
repository(owner:$owner, name:$name){
discussions(first:100, answered:true, states:[OPEN]) {
@@ -105,5 +96,4 @@ jobs:
}
await github.graphql(closeDiscussionMutation, closeVariables)
await sleep(1000)
}

View File

@@ -7,7 +7,7 @@
"trailingComma": "es5",
"overrides": [
{
"files": ["index.md", "administration.md"],
"files": "index.md",
"options": {
"tabWidth": 4
}

View File

@@ -20,7 +20,7 @@ djangorestframework = "~=3.14"
djangorestframework-guardian = "*"
drf-writable-nested = "*"
bleach = "*"
celery = {extras = ["redis"], version = "*"}
celery = "*"
channels = "~=4.0"
channels-redis = "*"
concurrent-log-handler = "*"

375
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "d7ef8db734997cda7c11971f2ddb66bf1918f4232b0956a9bf604c41763ce461"
"sha256": "f39e8ff3dbd6a7ad290d3fcad1efaf9252ffb498353527cb53126cf73e76824c"
},
"pipfile-spec": 6,
"requires": {},
@@ -24,11 +24,11 @@
},
"anyio": {
"hashes": [
"sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f",
"sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
"sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"
],
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
"version": "==4.0.0"
},
"asgiref": {
"hashes": [
@@ -156,19 +156,20 @@
"redis"
],
"hashes": [
"sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9",
"sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af"
"sha256:30b75ac60fb081c2d9f8881382c148ed7c9052031a75a1e8743ff4b4b071f184",
"sha256:6b65d8dd5db499dd6190c45aa6398e171b99592f2af62c312f7391587feb5458"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==5.3.6"
"version": "==5.3.5"
},
"certifi": {
"hashes": [
"sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1",
"sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
],
"markers": "python_version >= '3.6'",
"version": "==2023.11.17"
"version": "==2023.7.22"
},
"cffi": {
"hashes": [
@@ -384,32 +385,32 @@
},
"cryptography": {
"hashes": [
"sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960",
"sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a",
"sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc",
"sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a",
"sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf",
"sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1",
"sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39",
"sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406",
"sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a",
"sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a",
"sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c",
"sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be",
"sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15",
"sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2",
"sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d",
"sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157",
"sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003",
"sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248",
"sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a",
"sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec",
"sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309",
"sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7",
"sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"
"sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf",
"sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84",
"sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e",
"sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8",
"sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7",
"sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1",
"sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88",
"sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86",
"sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179",
"sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81",
"sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20",
"sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548",
"sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d",
"sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d",
"sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5",
"sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1",
"sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147",
"sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936",
"sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797",
"sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696",
"sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72",
"sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da",
"sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"
],
"markers": "python_version >= '3.7'",
"version": "==41.0.7"
"version": "==41.0.5"
},
"dateparser": {
"hashes": [
@@ -540,11 +541,11 @@
},
"exceptiongroup": {
"hashes": [
"sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
"sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
"sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
"sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
],
"markers": "python_version < '3.11'",
"version": "==1.2.0"
"version": "==1.1.3"
},
"filelock": {
"hashes": [
@@ -566,12 +567,12 @@
},
"gotenberg-client": {
"hashes": [
"sha256:69e9dd5264b75ed0ba1f9eebebdc750b13d190710fd82ca0670d161c249155c9",
"sha256:dd0f49d3d4e01399949f39ac5024a5512566c8ded6ee457a336a5f77ce4c1a25"
"sha256:4508ecb913ef2d553dd2ceb78e32cee001000ba08c910ba1f9ace38350d1589e",
"sha256:7a3f8a02caee768391373b3610c6ec25a853cccf391ed6b5d5a1292c3ed15e7e"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==0.4.1"
"version": "==0.3.0"
},
"gunicorn": {
"hashes": [
@@ -753,11 +754,11 @@
"http2"
],
"hashes": [
"sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8",
"sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"
"sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a",
"sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"
],
"markers": "python_version >= '3.9'",
"version": "==0.25.2"
"version": "==0.25.1"
},
"humanize": {
"hashes": [
@@ -777,11 +778,11 @@
},
"idna": {
"hashes": [
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.6"
"version": "==3.4"
},
"imap-tools": {
"hashes": [
@@ -793,9 +794,9 @@
},
"img2pdf": {
"hashes": [
"sha256:73847e47242f4b5bd113c70049e03e03212936c2727cd2a8bf564229a67d0b95"
"sha256:ae6c19731bde2551356c178bf356ca118ac32a232c737a14b423f8039df3c24b"
],
"version": "==0.5.1"
"version": "==0.5.0"
},
"inotify-simple": {
"hashes": [
@@ -823,11 +824,11 @@
},
"kombu": {
"hashes": [
"sha256:0bb2e278644d11dea6272c17974a3dbb9688a949f3bb60aeb5b791329c44fadc",
"sha256:63bb093fc9bb80cfb3a0972336a5cec1fa7ac5f9ef7e8237c6bf8dda9469313e"
"sha256:1491df826cfc5178c80f3e89dd6dfba68e484ef334db81070eb5cb8094b31167",
"sha256:6cd5c5d5ef77538434b8f81f3e265c414269418645dbb47dbf130a8a05c3e357"
],
"markers": "python_version >= '3.8'",
"version": "==5.3.4"
"version": "==5.3.3"
},
"langdetect": {
"hashes": [
@@ -1080,12 +1081,12 @@
},
"ocrmypdf": {
"hashes": [
"sha256:63803b21f80925170cc96dec11833a6cb740be377e50adfaa210b2c26e64b67a",
"sha256:9385f582fa8b9eb4d89a27dfd80a29cffdf53694c570f9f0e87db4af8cfe3b15"
"sha256:0ead16bb9842ac206d26a8c17d9acb76e9105e5fccecfc4cf5ced9dcc53f6d0a",
"sha256:e63015b8308be4f95e881f55a4cf5108696b748e0fd0e95de18d587a72f3b750"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==15.4.3"
"version": "==15.4.2"
},
"packaging": {
"hashes": [
@@ -1122,41 +1123,41 @@
},
"pikepdf": {
"hashes": [
"sha256:08070185661db9dfbaec014094fec9e323d8ab882dc458a07383f55e08bfdd22",
"sha256:0f118446960213c78b21fe106890028dcaff739369bceda9008719b43670e290",
"sha256:1e982dbf2d25df72975e2d336ca6a5a8565add156bfdba328beb67b324a1689e",
"sha256:2771cba79c62b3f9d93cffa684a92d60e6050f727d4f52220d8180eaf967c4bf",
"sha256:2a320bfa4caa73d00d90b04433305b3ff7a6c63bdf36f55af16725066fe46b49",
"sha256:3748c4d67b40c26fa777d1f3eefcab4bcca101395ad54502020653024d23c37d",
"sha256:3956b2abda0ef86b37b5d49066c3adfd29d056ed96b1ef93bacbd269c78c5e13",
"sha256:3b719a40f1b08da1ff6fd87b3847d4ce033f8ffd81a13dca8c3c947a080c071d",
"sha256:3ff178684ed43de28ca3e7dcf9da5082ebf49bcd042e9308ffaa0162275da1a4",
"sha256:42360df95f08164162af465c493d1e612d3c0ff6d23dc17be073380b05db715c",
"sha256:5075a2a741ba23212e963d9ea3fc7a4f9394535fab11fdc7ea4641d61cab0287",
"sha256:67276e1731dc8b2f58663ff62b4728f9e0046c8217768fe0649428315cfe8253",
"sha256:69d69a93d07027e351996ef8232f26bba762d415206414e0ae3814be0aee0f7a",
"sha256:7075c79fa5d85b0acf1536a1849bcbb4e661b33f476c7c6d81255df247b2ae1f",
"sha256:7bd88db0b68da1d04dd5a1ed0897e37ef1968317a887781b1e26c1649bbd11e3",
"sha256:80275466282495c672264801b50c7188f86590779f3ada8148336e3e84fd06c0",
"sha256:897cac14f9e3db97d4effea50190e6c59e87d4147da98ff6108d254dcc1949d9",
"sha256:8a1ac9e17d890b39f717b040eaebc79325ff23278756805220138f054599f9f0",
"sha256:8e687681f26533fee69904403fdf4fc2a1f0e47480b5bfbba64139c07d72c9d2",
"sha256:9430eb3439c0e9cb1b477115d5a34905f5c6be55422d66fcba55cedbf711ae89",
"sha256:94737db4beb7391ca78627e461050c5dd3a7cb7526fd6bdd0a0db94780e8b103",
"sha256:94998ff1d15b0eaec76494266e2e61d80573c5ee4ac0860a9af4a02e5107fe17",
"sha256:a9da8d1db2d7e17411afef351ea9ccddbff36ce748b16003d4f065250c941436",
"sha256:c11acef5c211af2559e1753ed96b3b4f30e7a9e26acad4b2cbd55ea3b25e3154",
"sha256:c631c6fd16b44a2636988d8be7202f1ffd3932f505e8b9044a6b88ef62411871",
"sha256:d0678ed952a85f4b3b23d5ee94445e55a05aec0b47de37843625eba1f9b0fbb3",
"sha256:d7899f43a198d184c3d4787390a67c654a8c600c6c844f29b557b4f236d11619",
"sha256:dc8f0116237d2f07fd8a8763f2c59f2fa50f445471f0c619914075ec411920e2",
"sha256:dd5ad4cb6244ac885512f5385f6fc055e2bfc174636189fc1408f138458380c8",
"sha256:e93d706b40cebc3b2fdaa9e814b4008ed4c659c6df03ee6a4ed46683aa21ab24",
"sha256:ece366b8e68be47a7252c27f73dd9666df6871fb6e302a7bfd00a5cb293743f2",
"sha256:f3781b8dc868e3d452ce36348b26735d7a2fc0fe6a4503f72411e5bc9341a7fa"
"sha256:07f6b4dfc492bd13cc18d9b6d5b0fcd57d82b1b7bffdc811e5de95722a6c9480",
"sha256:0b03167b53f465fbe1d658d5e3440fe1627bf21f0fe612bd3810d397634a2c1b",
"sha256:0c8b52f210513d34db8cea1960009bf8404c4d2f4f94a5b3e31d140fe33ddcb1",
"sha256:123c1e68c042fc1e51a6b8d0294a3bef1c091fcdc427abe71c9436fc1f9b51b2",
"sha256:261b0c107c659d9072879cb5837c38657966c8701c52d1a344b1176319d9a69a",
"sha256:26f40f9f9f113d85b0b6ac33a9539ca1c1cbf5a77c040e7c859cb71037c4c71d",
"sha256:39fcb93fb644f2e5fbb73142688fef701463f9c4d9a6af2d42c4ac9eca4e6f02",
"sha256:474eaa781216c467ee7e5b698a43f7d42012e277b16dd5b4ee6822955ad9c8a6",
"sha256:4b67956b6faceccd9da1d5a513bc3de8c5e2ee36e07cd8e4d24d639223ca6bbc",
"sha256:507e36c40568ff068e5209c34251168e19177fcba20ec8d83b5ae5340d20b0eb",
"sha256:54c0f76558da5c0f4779e31726fd5fa4ad6ec910ec319cd956bf638b5520f690",
"sha256:60a21302e03978a7f4c1fb6ef108eab4ac4f91aa0f7740f8636ead5ac9008b91",
"sha256:631b8f6b38bfe81977e0092f2e9a6c0bd5dd93510b809a277295c200a95f04b2",
"sha256:642525270546eef175e85bde4469eeaee767989f476ae3c129cb359b0a49159b",
"sha256:6a9dd1afa246c9e8c3f3acc96bb27b75666a87c16ffd9231f44b9379af52df48",
"sha256:6be2cee62932e77c2906e9594c538bf8092a72b54cabd0c995672506740a7afc",
"sha256:77d5f1b2b19a0a9f3aaf4114f12083e499cad5a94c19175d1e4fd19140e28143",
"sha256:7ca2f630f8ff2f4e9218c541fad320719bdc7f8ed9495e7d9f1b99075e7b382c",
"sha256:7f933fb675fa6de32f5d77963869a59da6a5536eaafc80dd1a0783457861c1dd",
"sha256:7fe08b60a13503b1e9481a84213c0db4e0c27effacb4740c2b4379c76820871f",
"sha256:80bbe725be2163b1301b355472d45f2857f12ff7bf259d39a282cf99eb094d5c",
"sha256:81261e533d513472084a46d044027c37c76561cf707106a86ecfb0b12d124ef2",
"sha256:b6b341c309cb0d992d10af3f4983ea69ce9d6f37274c474bf450fd0d8b4595fe",
"sha256:b7ab92e9f9e94c9bb264cdc622847fb3ece1b2c50ee99c6a6d35975cf3ec2bbe",
"sha256:b9e2bd27d57620c0f3a3801df0101f19ce53f0d9447a1a881940aeb773df5e87",
"sha256:c3a7f1fa5c5f806bdc01bb0b7c2ca6ac1f57b3a6fd1c15139a2b4afde3f40284",
"sha256:c729ce4d55485beaf1b526b55478ab15254eea704ecb1e40c9c9941c7448c297",
"sha256:cfc58963da2b83568d068ab5b93f90ea009c9ecf8e3e9e6aad26eaa6e7bf5905",
"sha256:d2af13e16b694e7913fe07386931bbf23964f74a6c0470136f9fb0fb6cac7b9b",
"sha256:e5ac975a421f308ca50fead5043474225275203d60a67153020cfc28e35cdfea",
"sha256:fa31e8ad1356e0469a6d80e388897fd5032f9044d412eb9cf6a24442449acc3f",
"sha256:ff55fee1061a5914c882e1120e8df591a0af6973db045305fc0432ef860f10c3"
],
"markers": "python_version >= '3.8'",
"version": "==8.7.1"
"version": "==8.7.0"
},
"pillow": {
"hashes": [
@@ -1244,11 +1245,11 @@
},
"prompt-toolkit": {
"hashes": [
"sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0",
"sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"
"sha256:99ba3dfb23d5b5af89712f89e60a5f3d9b8b67a9482ca377c5771d0e9047a34b",
"sha256:a371c06bb1d66cd499fecd708e50c0b6ae00acba9822ba33c586e2f16d1b739e"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==3.0.41"
"version": "==3.0.40"
},
"psycopg2": {
"hashes": [
@@ -1279,11 +1280,11 @@
},
"pygments": {
"hashes": [
"sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c",
"sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
],
"markers": "python_version >= '3.7'",
"version": "==2.17.2"
"version": "==2.16.1"
},
"python-dateutil": {
"hashes": [
@@ -1612,11 +1613,11 @@
},
"rich": {
"hashes": [
"sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa",
"sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"
"sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245",
"sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==13.7.0"
"version": "==13.6.0"
},
"scikit-learn": {
"hashes": [
@@ -1849,7 +1850,7 @@
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
],
"markers": "python_version < '3.10'",
"markers": "python_version < '3.11'",
"version": "==4.8.0"
},
"tzdata": {
@@ -2039,10 +2040,10 @@
},
"wcwidth": {
"hashes": [
"sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02",
"sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"
"sha256:9a929bd8380f6cd9571a968a9c8f4353ca58d7cd812a4822bba831f8d685b223",
"sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8"
],
"version": "==0.2.12"
"version": "==0.2.9"
},
"webencodings": {
"hashes": [
@@ -2300,11 +2301,11 @@
"develop": {
"anyio": {
"hashes": [
"sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f",
"sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
"sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"
],
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
"version": "==4.0.0"
},
"asgiref": {
"hashes": [
@@ -2371,11 +2372,11 @@
},
"certifi": {
"hashes": [
"sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1",
"sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
],
"markers": "python_version >= '3.6'",
"version": "==2023.11.17"
"version": "==2023.7.22"
},
"cffi": {
"hashes": [
@@ -2671,11 +2672,11 @@
},
"exceptiongroup": {
"hashes": [
"sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14",
"sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"
"sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
"sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
],
"markers": "python_version < '3.11'",
"version": "==1.2.0"
"version": "==1.1.3"
},
"execnet": {
"hashes": [
@@ -2707,6 +2708,7 @@
"sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
"sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==3.13.1"
},
@@ -2738,11 +2740,11 @@
"http2"
],
"hashes": [
"sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8",
"sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"
"sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a",
"sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"
],
"markers": "python_version >= '3.8'",
"version": "==0.25.2"
"markers": "python_version >= '3.9'",
"version": "==0.25.1"
},
"hyperlink": {
"hashes": [
@@ -2753,19 +2755,19 @@
},
"identify": {
"hashes": [
"sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d",
"sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"
"sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75",
"sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"
],
"markers": "python_version >= '3.8'",
"version": "==2.5.33"
"version": "==2.5.31"
},
"idna": {
"hashes": [
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.6"
"version": "==3.4"
},
"imagehash": {
"hashes": [
@@ -2777,11 +2779,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7",
"sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"
"sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb",
"sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"
],
"markers": "python_version < '3.10'",
"version": "==7.0.0"
"version": "==6.8.0"
},
"incremental": {
"hashes": [
@@ -2898,28 +2900,28 @@
},
"mkdocs-glightbox": {
"hashes": [
"sha256:096c2753cf4f46f548b02070a2ff5dd8b823a431ce17873a62dcef304cf3364c",
"sha256:f572256cca17c912da50a045129026566a79b8c6477e1170258ccc0ac5b162da"
"sha256:8f894435b4f75231164e5d9fb023c01e922e6769e74a121e822c4914f310a41d",
"sha256:96aaf98216f83c0d0fad2e42a8d805cfa6329d6ab25b54265012ccb2154010d8"
],
"index": "pypi",
"version": "==0.3.5"
"version": "==0.3.4"
},
"mkdocs-material": {
"hashes": [
"sha256:6ed0fbf4682491766f0ec1acc955db6901c2fd424c7ab343964ef51b819741f5",
"sha256:ca8b9cd2b3be53e858e5a1a45ac9668bd78d95d77a30288bb5ebc1a31db6184c"
"sha256:8b20f6851bddeef37dced903893cd176cf13a21a482e97705a103c45f06ce9b9",
"sha256:f0c101453e8bc12b040e8b64ca39a405d950d8402609b1378cc2b98976e74b5f"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==9.5.2"
"version": "==9.4.8"
},
"mkdocs-material-extensions": {
"hashes": [
"sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443",
"sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"
"sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc",
"sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd"
],
"markers": "python_version >= '3.8'",
"version": "==1.3.1"
"version": "==1.3"
},
"mypy-extensions": {
"hashes": [
@@ -2995,11 +2997,11 @@
},
"pathspec": {
"hashes": [
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
"sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
"sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
],
"markers": "python_version >= '3.8'",
"version": "==0.12.1"
"markers": "python_version >= '3.7'",
"version": "==0.11.2"
},
"pillow": {
"hashes": [
@@ -3063,11 +3065,11 @@
},
"platformdirs": {
"hashes": [
"sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380",
"sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
],
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
"markers": "python_version >= '3.7'",
"version": "==3.11.0"
},
"pluggy": {
"hashes": [
@@ -3079,12 +3081,12 @@
},
"pre-commit": {
"hashes": [
"sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376",
"sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"
"sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32",
"sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==3.6.0"
"markers": "python_version >= '3.8'",
"version": "==3.5.0"
},
"pyasn1": {
"hashes": [
@@ -3111,19 +3113,19 @@
},
"pygments": {
"hashes": [
"sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c",
"sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
],
"markers": "python_version >= '3.7'",
"version": "==2.17.2"
"version": "==2.16.1"
},
"pymdown-extensions": {
"hashes": [
"sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b",
"sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879"
"sha256:bc46f11749ecd4d6b71cf62396104b4a200bad3498cb0f5dad1b8502fe461a35",
"sha256:cfc28d6a09d19448bcbf8eee3ce098c7d17ff99f7bd3069db4819af181212037"
],
"markers": "python_version >= '3.8'",
"version": "==10.5"
"version": "==10.4"
},
"pyopenssl": {
"hashes": [
@@ -3161,30 +3163,30 @@
},
"pytest-env": {
"hashes": [
"sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc",
"sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"
"sha256:1efb8acce1f6431196150f3b30673443ff05a6fabff64539a9495cd2248adf9e",
"sha256:2b71b37c6810f28bec790a7b373c777af87352b3a359b3de0edb9d24df5cf8b3"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.1.3"
"version": "==1.1.1"
},
"pytest-httpx": {
"hashes": [
"sha256:24f6f53d507ab483bea8f89b975a1a111fb613ccab4d86e570be8991776e8bcc",
"sha256:a33c4e8df415cc1232b3664869b6a8b8061c4c223335aca0b237cefbc01ba0eb"
"sha256:b489c5a7bb847551943eaee601bc35053b35dc4f5961c944305120f14a1d770a",
"sha256:ca372b94c569c0aca2f06240f6f78cc223dfbc3ab97b5700d4e14c9a73eab17a"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==0.27.0"
"version": "==0.26.0"
},
"pytest-rerunfailures": {
"hashes": [
"sha256:34919cb3fcb1f8e5d4b940aa75ccdea9661bade925091873b7c6fa5548333069",
"sha256:e132dbe420bc476f544b96e7036edd0a69707574209b6677263c950d19b09199"
"sha256:784f462fa87fe9bdf781d0027d856b47a4bfe6c12af108f6bd887057a917b48e",
"sha256:9a1afd04e21b8177faf08a9bbbf44de7a0fe3fc29f8ddbe83b9684bd5f8f92a9"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==13.0"
"version": "==12.0"
},
"pytest-sugar": {
"hashes": [
@@ -3196,18 +3198,19 @@
},
"pytest-xdist": {
"hashes": [
"sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a",
"sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"
"sha256:3a94a931dd9e268e0b871a877d09fe2efb6175c2c23d60d56a6001359002b832",
"sha256:e513118bf787677a427e025606f55e95937565e06dfaac8d87f55301e57ae607"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==3.5.0"
"version": "==3.4.0"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
@@ -3295,7 +3298,6 @@
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
],
"markers": "python_version >= '3.6'",
"version": "==6.0.1"
},
"pyyaml-env-tag": {
@@ -3410,27 +3412,27 @@
},
"ruff": {
"hashes": [
"sha256:0683b7bfbb95e6df3c7c04fe9d78f631f8e8ba4868dfc932d43d690698057e2e",
"sha256:1ea109bdb23c2a4413f397ebd8ac32cb498bee234d4191ae1a310af760e5d287",
"sha256:276a89bcb149b3d8c1b11d91aa81898fe698900ed553a08129b38d9d6570e717",
"sha256:290ecab680dce94affebefe0bbca2322a6277e83d4f29234627e0f8f6b4fa9ce",
"sha256:416dfd0bd45d1a2baa3b1b07b1b9758e7d993c256d3e51dc6e03a5e7901c7d80",
"sha256:45b38c3f8788a65e6a2cab02e0f7adfa88872696839d9882c13b7e2f35d64c5f",
"sha256:4af95fd1d3b001fc41325064336db36e3d27d2004cdb6d21fd617d45a172dd96",
"sha256:69a4bed13bc1d5dabf3902522b5a2aadfebe28226c6269694283c3b0cecb45fd",
"sha256:6b05e3b123f93bb4146a761b7a7d57af8cb7384ccb2502d29d736eaade0db519",
"sha256:6c64cb67b2025b1ac6d58e5ffca8f7b3f7fd921f35e78198411237e4f0db8e73",
"sha256:7f80496854fdc65b6659c271d2c26e90d4d401e6a4a31908e7e334fab4645aac",
"sha256:8b0c2de9dd9daf5e07624c24add25c3a490dbf74b0e9bca4145c632457b3b42a",
"sha256:90c958fe950735041f1c80d21b42184f1072cc3975d05e736e8d66fc377119ea",
"sha256:9dcc6bb2f4df59cb5b4b40ff14be7d57012179d69c6565c1da0d1f013d29951b",
"sha256:de02ca331f2143195a712983a57137c5ec0f10acc4aa81f7c1f86519e52b92a1",
"sha256:df2bb4bb6bbe921f6b4f5b6fdd8d8468c940731cb9406f274ae8c5ed7a78c478",
"sha256:dffd699d07abf54833e5f6cc50b85a6ff043715da8788c4a79bcd4ab4734d306"
"sha256:171276c1df6c07fa0597fb946139ced1c2978f4f0b8254f201281729981f3c17",
"sha256:17ef33cd0bb7316ca65649fc748acc1406dfa4da96a3d0cde6d52f2e866c7b39",
"sha256:32d47fc69261c21a4c48916f16ca272bf2f273eb635d91c65d5cd548bf1f3d96",
"sha256:5cbec0ef2ae1748fb194f420fb03fb2c25c3258c86129af7172ff8f198f125ab",
"sha256:721f4b9d3b4161df8dc9f09aa8562e39d14e55a4dbaa451a8e55bdc9590e20f4",
"sha256:82bfcb9927e88c1ed50f49ac6c9728dab3ea451212693fe40d08d314663e412f",
"sha256:9b97fd6da44d6cceb188147b68db69a5741fbc736465b5cea3928fdac0bc1aeb",
"sha256:a00a7ec893f665ed60008c70fe9eeb58d210e6b4d83ec6654a9904871f982a2a",
"sha256:a4894dddb476597a0ba4473d72a23151b8b3b0b5f958f2cf4d3f1c572cdb7af7",
"sha256:a8c11206b47f283cbda399a654fd0178d7a389e631f19f51da15cbe631480c5b",
"sha256:aafb9d2b671ed934998e881e2c0f5845a4295e84e719359c71c39a5363cccc91",
"sha256:b2c205827b3f8c13b4a432e9585750b93fd907986fe1aec62b2a02cf4401eee6",
"sha256:bb408e3a2ad8f6881d0f2e7ad70cddb3ed9f200eb3517a91a245bbe27101d379",
"sha256:c21fe20ee7d76206d290a76271c1af7a5096bc4c73ab9383ed2ad35f852a0087",
"sha256:f20dc5e5905ddb407060ca27267c7174f532375c08076d1a953cf7bb016f5a24",
"sha256:f80c73bba6bc69e4fdc73b3991db0b546ce641bdcd5b07210b8ad6f64c79f1ab",
"sha256:fa29e67b3284b9a79b1a85ee66e293a94ac6b7bb068b307a8a373c3d343aa8ec"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==0.1.7"
"version": "==0.1.5"
},
"scipy": {
"hashes": [
@@ -3472,11 +3474,11 @@
},
"setuptools": {
"hashes": [
"sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2",
"sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"
"sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
"sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
],
"markers": "python_version >= '3.8'",
"version": "==69.0.2"
"version": "==68.2.2"
},
"six": {
"hashes": [
@@ -3547,11 +3549,11 @@
},
"virtualenv": {
"hashes": [
"sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3",
"sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"
"sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af",
"sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"
],
"markers": "python_version >= '3.7'",
"version": "==20.25.0"
"version": "==20.24.6"
},
"watchdog": {
"hashes": [
@@ -3583,6 +3585,7 @@
"sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44",
"sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==3.0.0"
},

View File

@@ -6,11 +6,8 @@
[![demo](https://cronitor.io/badges/ve7ItY/production/W5E_B9jkelG9ZbDiNHUPQEVH3MY.svg)](https://demo.paperless-ngx.com)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/resources/logo/web/png/White%20logo%20-%20no%20background.png" width="50%">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
</picture>
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png#gh-light-mode-only" width="50%" />
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/White%20logo%20-%20no%20background.png#gh-dark-mode-only" width="50%" />
</p>
<!-- omit in toc -->
@@ -35,19 +32,16 @@ A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com)
# Features
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards-dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png">
<img src="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png">
</picture>
![Dashboard](https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png#gh-light-mode-only)
![Dashboard](https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards-dark.png#gh-dark-mode-only)
A full list of [features](https://docs.paperless-ngx.com/#features) and [screenshots](https://docs.paperless-ngx.com/#screenshots) are available in the [documentation](https://docs.paperless-ngx.com/).
# Getting started
The easiest way to deploy paperless is `docker compose`. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from GitHub Packages.
The easiest way to deploy paperless is docker-compose. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from GitHub Packages.
If you'd like to jump right in, you can configure a `docker compose` environment with our install script:
If you'd like to jump right in, you can configure a docker-compose environment with our install script:
```bash
bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"

View File

@@ -1,6 +1,8 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
commit_message: '[ci skip]'
pull_request_labels: [
"skip-changelog",
"translation"
]
files:
- source: /src/locale/en_US/LC_MESSAGES/django.po
translation: /src/locale/%locale_with_underscore%/LC_MESSAGES/django.po

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless testing with actual gotenberg
# docker-compose file for running paperless testing with actual gotenberg
# and Tika containers for a more end to end test of the Tika related functionality
# Can be used locally or by the CI to start the nessecary containers with the
# correct networking for the tests
@@ -6,7 +6,7 @@
version: "3.7"
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:7.10
image: docker.io/gotenberg/gotenberg:7.8
hostname: gotenberg
container_name: gotenberg
network_mode: host
@@ -17,8 +17,6 @@ services:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
- "--log-level=warn"
- "--log-format=text"
tika:
image: ghcr.io/paperless-ngx/tika:latest
hostname: tika

View File

@@ -1,4 +1,4 @@
# docker compose file for running paperless from the Docker Hub.
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
@@ -10,7 +10,7 @@
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), MariaDB is used as the database server.
@@ -23,9 +23,9 @@
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
@@ -83,7 +83,7 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:7.10
image: docker.io/gotenberg/gotenberg:7.8
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless from the Docker Hub.
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
@@ -10,7 +10,7 @@
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), MariaDB is used as the database server.
@@ -19,9 +19,9 @@
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless from the Docker Hub.
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
@@ -10,7 +10,7 @@
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8010.
#
# In addition to that, this Docker Compose file adds the following optional
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless from the docker container registry.
# docker-compose file for running paperless from the docker container registry.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
@@ -10,7 +10,7 @@
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
@@ -23,9 +23,9 @@
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
@@ -77,7 +77,7 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:7.10
image: docker.io/gotenberg/gotenberg:7.8
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless from the Docker Hub.
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
@@ -10,7 +10,7 @@
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
@@ -19,9 +19,9 @@
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless from the docker container registry.
# docker-compose file for running paperless from the docker container registry.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
# All compose files of paperless configure paperless in the following way:
@@ -11,7 +11,7 @@
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# In addition to that, this Docker Compose file adds the following optional
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Apache Tika and Gotenberg servers are started with paperless and paperless
@@ -23,9 +23,9 @@
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
@@ -65,7 +65,7 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:7.10
image: docker.io/gotenberg/gotenberg:7.8
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not

View File

@@ -1,4 +1,4 @@
# Docker Compose file for running paperless from the Docker Hub.
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
@@ -16,9 +16,9 @@
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker compose up -d'.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.

View File

@@ -5,19 +5,17 @@
Multiple options exist for making backups of your paperless instance,
depending on how you installed paperless.
Before making a backup, it's probably best to make sure that paperless is not actively
consuming documents at that time.
Before making backups, make sure that paperless is not running.
Options available to any installation of paperless:
- Use the [document exporter](#exporter). The document exporter exports all your documents,
thumbnails, metadata, and database contents to a specific folder. You may import your
documents and settings into a fresh instance of paperless again or store your
documents in another DMS with this export.
The document exporter is also able to update an already existing
export. Therefore, incremental backups with `rsync` are entirely
possible.
- Use the [document exporter](#exporter). The document exporter exports all your documents,
thumbnails, metadata, and database contents to a specific folder. You may import your
documents and settings into a fresh instance of paperless again or store your
documents in another DMS with this export.
- The document exporter is also able to update an already existing
export. Therefore, incremental backups with `rsync` are entirely
possible.
!!! caution
@@ -27,37 +25,31 @@ Options available to any installation of paperless:
Options available to docker installations:
- Backup the docker volumes. These usually reside within
`/var/lib/docker/volumes` on the host and you need to be root in
order to access them.
- Backup the docker volumes. These usually reside within
`/var/lib/docker/volumes` on the host and you need to be root in
order to access them.
Paperless uses 4 volumes:
Paperless uses 4 volumes:
- `paperless_media`: This is where your documents are stored.
- `paperless_data`: This is where auxiliary data is stored. This
folder also contains the SQLite database, if you use it.
- `paperless_pgdata`: Exists only if you use PostgreSQL and
contains the database.
- `paperless_dbdata`: Exists only if you use MariaDB and contains
the database.
- `paperless_media`: This is where your documents are stored.
- `paperless_data`: This is where auxillary data is stored. This
folder also contains the SQLite database, if you use it.
- `paperless_pgdata`: Exists only if you use PostgreSQL and
contains the database.
- `paperless_dbdata`: Exists only if you use MariaDB and contains
the database.
Options available to bare-metal and non-docker installations:
- Backup the entire paperless folder. This ensures that if your
paperless instance crashes at some point or your disk fails, you can
simply copy the folder back into place and it works.
- Backup the entire paperless folder. This ensures that if your
paperless instance crashes at some point or your disk fails, you can
simply copy the folder back into place and it works.
When using PostgreSQL or MariaDB, you'll also have to backup the
database.
When using PostgreSQL or MariaDB, you'll also have to backup the
database.
### Restoring {#migrating-restoring}
If you've backed-up Paperless-ngx using the [document exporter](#exporter),
restoring can simply be done with the [document importer](#importer).
Of course, other backup strategies require restoring any volumes, folders and database
copies you created in the steps above.
## Updating Paperless {#updating}
### Docker Route {#docker-updating}
@@ -71,7 +63,7 @@ First of all, ensure that paperless is stopped.
```shell-session
$ cd /path/to/paperless
$ docker compose down
$ docker-compose down
```
After that, [make a backup](#backup).
@@ -79,22 +71,22 @@ After that, [make a backup](#backup).
1. If you pull the image from the docker hub, all you need to do is:
```shell-session
$ docker compose pull
$ docker compose up
$ docker-compose pull
$ docker-compose up
```
The Docker Compose files refer to the `latest` version, which is
The docker-compose files refer to the `latest` version, which is
always the latest stable release.
1. If you built the image yourself, do the following:
```shell-session
$ git pull
$ docker compose build
$ docker compose up
$ docker-compose build
$ docker-compose up
```
Running `docker compose up` will also apply any new database migrations.
Running `docker-compose up` will also apply any new database migrations.
If you see everything working, press CTRL+C once to gracefully stop
paperless. Then you can start paperless-ngx with `-d` to have it run in
the background.
@@ -102,7 +94,7 @@ the background.
!!! note
In version 0.9.14, the update process was changed. In 0.9.13 and
earlier, the Docker Compose files specified exact versions and pull
earlier, the docker-compose files specified exact versions and pull
won't automatically update to newer versions. In order to enable
updates as described above, either get the new `docker-compose.yml`
file from
@@ -220,11 +212,11 @@ Paperless comes with some management commands that perform various
maintenance tasks on your paperless instance. You can invoke these
commands in the following way:
With Docker Compose, while paperless is running:
With docker-compose, while paperless is running:
```shell-session
$ cd /path/to/paperless
$ docker compose exec webserver <command> <arguments>
$ docker-compose exec webserver <command> <arguments>
```
With docker, while paperless is running:
@@ -254,7 +246,7 @@ migration to another DMS.
If you use the document exporter within a cronjob to backup your data
you might use the `-T` flag behind exec to suppress "The input device
is not a TTY" errors. For example:
`docker compose exec -T webserver document_exporter ../export`
`docker-compose exec -T webserver document_exporter ../export`
```
document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z]
@@ -408,7 +400,7 @@ that don't match a document anymore get removed as well.
### Managing the Automatic matching algorithm
The _Auto_ matching algorithm requires a trained neural network to work.
This network needs to be updated whenever something in your data
This network needs to be updated whenever somethings in your data
changes. The docker image takes care of that automatically with the task
scheduler. You can manually renew the classifier by invoking the
following management command:
@@ -478,19 +470,19 @@ collection for issues.
The issues detected by the sanity checker are as follows:
- Missing original files.
- Missing archive files.
- Inaccessible original files due to improper permissions.
- Inaccessible archive files due to improper permissions.
- Corrupted original documents by comparing their checksum against
what is stored in the database.
- Corrupted archive documents by comparing their checksum against what
is stored in the database.
- Missing thumbnails.
- Inaccessible thumbnails due to improper permissions.
- Documents without any content (warning).
- Orphaned files in the media directory (warning). These are files
that are not referenced by any document in paperless.
- Missing original files.
- Missing archive files.
- Inaccessible original files due to improper permissions.
- Inaccessible archive files due to improper permissions.
- Corrupted original documents by comparing their checksum against
what is stored in the database.
- Corrupted archive documents by comparing their checksum against what
is stored in the database.
- Missing thumbnails.
- Inaccessible thumbnails due to improper permissions.
- Documents without any content (warning).
- Orphaned files in the media directory (warning). These are files
that are not referenced by any document in paperless.
```
document_sanity_checker
@@ -597,7 +589,7 @@ This tool does a fuzzy match over document content, looking for
those which look close according to a given ratio.
At this time, other metadata (such as correspondent or type) is not
taken into account by the detection.
take into account by the detection.
```
document_fuzzy_match [--ratio] [--processes N]

View File

@@ -236,8 +236,8 @@ webserver:
Troubleshooting:
- Monitor the Docker Compose log
`cd ~/paperless-ngx; docker compose logs -f`
- Monitor the docker-compose log
`cd ~/paperless-ngx; docker-compose logs -f`
- Check your script's permission e.g. in case of permission error
`sudo chmod 755 post-consumption-example.sh`
- Pipe your scripts's output to a log file e.g.
@@ -510,7 +510,7 @@ existing tables) with:
## Barcodes {#barcodes}
Paperless is able to utilize barcodes for automatically performing some tasks.
Paperless is able to utilize barcodes for automatically preforming some tasks.
At this time, the library utilized for detection of barcodes supports the following types:
@@ -566,7 +566,7 @@ collating two separate scans into one document, reordering the pages as necessar
Suppose you have a double-sided document with 6 pages (3 sheets of paper). First,
put the stack into your ADF as normal, ensuring that page 1 is scanned first. Your ADF
will now scan pages 1, 3, and 5. Then you (or your scanner, if it supports it) upload
will now scan pages 1, 3, and 5. Then you (or your the scanner, if it supports it) upload
the scan into the correct sub-directory of the consume folder (`double-sided` by default;
keep in mind that Paperless will _not_ automatically create the directory for you.)
Paperless will then process the scan and move it into an internal staging area.

View File

@@ -21,7 +21,6 @@ The API provides the following main endpoints:
- `/api/groups/`: Full CRUD support.
- `/api/share_links/`: Full CRUD support.
- `/api/custom_fields/`: Full CRUD support.
- `/api/profile/`: GET, PATCH
All of these endpoints except for the logging endpoint allow you to
fetch (and edit and delete where appropriate) individual objects by
@@ -54,7 +53,7 @@ fields:
- `set_permissions`: Allows setting document permissions. Optional,
write-only. See [below](#permissions).
- `custom_fields`: Array of custom fields & values, specified as
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
{ field: CUSTOM_FIELD_ID, value: VALUE }
## Downloading documents
@@ -158,10 +157,6 @@ The REST api provides three different forms of authentication.
3. Token authentication
You can create (or re-create) an API token by opening the "My Profile"
link in the user dropdown found in the web UI and clicking the circular
arrow button.
Paperless also offers an endpoint to acquire authentication tokens.
POST a username and password as a form or json string to
@@ -173,7 +168,7 @@ The REST api provides three different forms of authentication.
Authorization: Token <token>
```
Tokens can also be managed in the Django admin.
Tokens can be managed and revoked in the paperless admin.
## Searching for documents

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -1,440 +1,5 @@
# Changelog
## paperless-ngx 2.1.2
### Bug Fixes
- Fix: sort consumption templates by order by default [@shamoon](https://github.com/shamoon) ([#4956](https://github.com/paperless-ngx/paperless-ngx/pull/4956))
- Fix: Updates gotenberg-client, including workaround for Gotenberg non-latin handling [@stumpylog](https://github.com/stumpylog) ([#4944](https://github.com/paperless-ngx/paperless-ngx/pull/4944))
- Fix: allow text copy in pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#4938](https://github.com/paperless-ngx/paperless-ngx/pull/4938))
- Fix: Don't allow autocomplete searches to fail on schema field matches [@stumpylog](https://github.com/stumpylog) ([#4934](https://github.com/paperless-ngx/paperless-ngx/pull/4934))
- Fix: Convert search dates to UTC in advanced search [@bogdal](https://github.com/bogdal) ([#4891](https://github.com/paperless-ngx/paperless-ngx/pull/4891))
- Fix: Use the attachment filename so downstream template matching works [@stumpylog](https://github.com/stumpylog) ([#4931](https://github.com/paperless-ngx/paperless-ngx/pull/4931))
- Fix: frontend handle autocomplete failure gracefully [@shamoon](https://github.com/shamoon) ([#4903](https://github.com/paperless-ngx/paperless-ngx/pull/4903))
### Dependencies
- Chore(deps-dev): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#4942](https://github.com/paperless-ngx/paperless-ngx/pull/4942))
- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#4939](https://github.com/paperless-ngx/paperless-ngx/pull/4939))
### All App Changes
<details>
<summary>9 changes</summary>
- Fix: sort consumption templates by order by default [@shamoon](https://github.com/shamoon) ([#4956](https://github.com/paperless-ngx/paperless-ngx/pull/4956))
- Chore: reorganize api tests [@shamoon](https://github.com/shamoon) ([#4935](https://github.com/paperless-ngx/paperless-ngx/pull/4935))
- Chore(deps-dev): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#4942](https://github.com/paperless-ngx/paperless-ngx/pull/4942))
- Fix: allow text copy in pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#4938](https://github.com/paperless-ngx/paperless-ngx/pull/4938))
- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#4939](https://github.com/paperless-ngx/paperless-ngx/pull/4939))
- Fix: Don't allow autocomplete searches to fail on schema field matches [@stumpylog](https://github.com/stumpylog) ([#4934](https://github.com/paperless-ngx/paperless-ngx/pull/4934))
- Fix: Convert search dates to UTC in advanced search [@bogdal](https://github.com/bogdal) ([#4891](https://github.com/paperless-ngx/paperless-ngx/pull/4891))
- Fix: Use the attachment filename so downstream template matching works [@stumpylog](https://github.com/stumpylog) ([#4931](https://github.com/paperless-ngx/paperless-ngx/pull/4931))
- Fix: frontend handle autocomplete failure gracefully [@shamoon](https://github.com/shamoon) ([#4903](https://github.com/paperless-ngx/paperless-ngx/pull/4903))
</details>
## paperless-ngx 2.1.1
### Bug Fixes
- Fix: disable toggle for share link creation without archive version, fix auto-copy in Safari [@shamoon](https://github.com/shamoon) ([#4885](https://github.com/paperless-ngx/paperless-ngx/pull/4885))
- Fix: storage paths link incorrect in dashboard widget [@shamoon](https://github.com/shamoon) ([#4878](https://github.com/paperless-ngx/paperless-ngx/pull/4878))
- Fix: respect baseURI for pdfjs worker URL [@shamoon](https://github.com/shamoon) ([#4865](https://github.com/paperless-ngx/paperless-ngx/pull/4865))
- Fix: Allow users to configure the From email for password reset [@stumpylog](https://github.com/stumpylog) ([#4867](https://github.com/paperless-ngx/paperless-ngx/pull/4867))
- Fix: dont show move icon for file tasks badge [@shamoon](https://github.com/shamoon) ([#4860](https://github.com/paperless-ngx/paperless-ngx/pull/4860))
### Maintenance
- Chore: Simplifies how the documentation site is deployed [@stumpylog](https://github.com/stumpylog) ([#4858](https://github.com/paperless-ngx/paperless-ngx/pull/4858))
### All App Changes
<details>
<summary>5 changes</summary>
- Fix: disable toggle for share link creation without archive version, fix auto-copy in Safari [@shamoon](https://github.com/shamoon) ([#4885](https://github.com/paperless-ngx/paperless-ngx/pull/4885))
- Fix: storage paths link incorrect in dashboard widget [@shamoon](https://github.com/shamoon) ([#4878](https://github.com/paperless-ngx/paperless-ngx/pull/4878))
- Fix: respect baseURI for pdfjs worker URL [@shamoon](https://github.com/shamoon) ([#4865](https://github.com/paperless-ngx/paperless-ngx/pull/4865))
- Fix: Allow users to configure the From email for password reset [@stumpylog](https://github.com/stumpylog) ([#4867](https://github.com/paperless-ngx/paperless-ngx/pull/4867))
- Fix: dont show move icon for file tasks badge [@shamoon](https://github.com/shamoon) ([#4860](https://github.com/paperless-ngx/paperless-ngx/pull/4860))
</details>
## paperless-ngx 2.1.0
### Features
- Enhancement: implement document link custom field [@shamoon](https://github.com/shamoon) ([#4799](https://github.com/paperless-ngx/paperless-ngx/pull/4799))
- Feature: Adds additional warnings during an import if it might fail [@stumpylog](https://github.com/stumpylog) ([#4814](https://github.com/paperless-ngx/paperless-ngx/pull/4814))
- Feature: pngx PDF viewer with updated pdfjs [@shamoon](https://github.com/shamoon) ([#4679](https://github.com/paperless-ngx/paperless-ngx/pull/4679))
- Enhancement: support automatically assigning custom fields via consumption templates [@shamoon](https://github.com/shamoon) ([#4727](https://github.com/paperless-ngx/paperless-ngx/pull/4727))
- Feature: update user profile [@shamoon](https://github.com/shamoon) ([#4678](https://github.com/paperless-ngx/paperless-ngx/pull/4678))
- Enhancement: Allow excluding mail attachments by name [@stumpylog](https://github.com/stumpylog) ([#4691](https://github.com/paperless-ngx/paperless-ngx/pull/4691))
- Enhancement: auto-refresh logs \& tasks [@shamoon](https://github.com/shamoon) ([#4680](https://github.com/paperless-ngx/paperless-ngx/pull/4680))
### Bug Fixes
- Fix: welcome widget text color [@shamoon](https://github.com/shamoon) ([#4829](https://github.com/paperless-ngx/paperless-ngx/pull/4829))
- Fix: export consumption templates \& custom fields in exporter [@shamoon](https://github.com/shamoon) ([#4825](https://github.com/paperless-ngx/paperless-ngx/pull/4825))
- Fix: bulk edit object permissions should use permissions object [@shamoon](https://github.com/shamoon) ([#4797](https://github.com/paperless-ngx/paperless-ngx/pull/4797))
- Fix: empty string for consumption template field should be interpreted as [@shamoon](https://github.com/shamoon) ([#4762](https://github.com/paperless-ngx/paperless-ngx/pull/4762))
- Fix: use default permissions for objects created via dropdown [@shamoon](https://github.com/shamoon) ([#4778](https://github.com/paperless-ngx/paperless-ngx/pull/4778))
- Fix: Alpha layer removal could allow duplicates [@stumpylog](https://github.com/stumpylog) ([#4781](https://github.com/paperless-ngx/paperless-ngx/pull/4781))
- Fix: update checker broke in v2.0.0 [@shamoon](https://github.com/shamoon) ([#4773](https://github.com/paperless-ngx/paperless-ngx/pull/4773))
- Fix: only show global drag-drop when files included [@shamoon](https://github.com/shamoon) ([#4767](https://github.com/paperless-ngx/paperless-ngx/pull/4767))
### Documentation
- Enhancement: implement document link custom field [@shamoon](https://github.com/shamoon) ([#4799](https://github.com/paperless-ngx/paperless-ngx/pull/4799))
- Fix: export consumption templates \& custom fields in exporter [@shamoon](https://github.com/shamoon) ([#4825](https://github.com/paperless-ngx/paperless-ngx/pull/4825))
- Documentation: Fix typos [@omahs](https://github.com/omahs) ([#4737](https://github.com/paperless-ngx/paperless-ngx/pull/4737))
### Maintenance
- Bump the actions group with 2 updates [@dependabot](https://github.com/dependabot) ([#4745](https://github.com/paperless-ngx/paperless-ngx/pull/4745))
### Dependencies
<details>
<summary>7 changes</summary>
- Bump the development group with 6 updates [@dependabot](https://github.com/dependabot) ([#4838](https://github.com/paperless-ngx/paperless-ngx/pull/4838))
- Bump the actions group with 2 updates [@dependabot](https://github.com/dependabot) ([#4745](https://github.com/paperless-ngx/paperless-ngx/pull/4745))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4756](https://github.com/paperless-ngx/paperless-ngx/pull/4756))
- Bump the frontend-jest-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4744](https://github.com/paperless-ngx/paperless-ngx/pull/4744))
- Bump [@<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot) ([#4749](https://github.com/paperless-ngx/paperless-ngx/pull/4749))
- Bump wait-on from 7.0.1 to 7.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#4747](https://github.com/paperless-ngx/paperless-ngx/pull/4747))
- Bump [@<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot) ([#4748](https://github.com/paperless-ngx/paperless-ngx/pull/4748))
</details>
### All App Changes
<details>
<summary>20 changes</summary>
- Enhancement: implement document link custom field [@shamoon](https://github.com/shamoon) ([#4799](https://github.com/paperless-ngx/paperless-ngx/pull/4799))
- Bump the development group with 6 updates [@dependabot](https://github.com/dependabot) ([#4838](https://github.com/paperless-ngx/paperless-ngx/pull/4838))
- Fix: welcome widget text color [@shamoon](https://github.com/shamoon) ([#4829](https://github.com/paperless-ngx/paperless-ngx/pull/4829))
- Fix: export consumption templates \& custom fields in exporter [@shamoon](https://github.com/shamoon) ([#4825](https://github.com/paperless-ngx/paperless-ngx/pull/4825))
- Feature: Adds additional warnings during an import if it might fail [@stumpylog](https://github.com/stumpylog) ([#4814](https://github.com/paperless-ngx/paperless-ngx/pull/4814))
- Feature: pngx PDF viewer with updated pdfjs [@shamoon](https://github.com/shamoon) ([#4679](https://github.com/paperless-ngx/paperless-ngx/pull/4679))
- Fix: bulk edit object permissions should use permissions object [@shamoon](https://github.com/shamoon) ([#4797](https://github.com/paperless-ngx/paperless-ngx/pull/4797))
- Enhancement: support automatically assigning custom fields via consumption templates [@shamoon](https://github.com/shamoon) ([#4727](https://github.com/paperless-ngx/paperless-ngx/pull/4727))
- Fix: empty string for consumption template field should be interpreted as [@shamoon](https://github.com/shamoon) ([#4762](https://github.com/paperless-ngx/paperless-ngx/pull/4762))
- Fix: use default permissions for objects created via dropdown [@shamoon](https://github.com/shamoon) ([#4778](https://github.com/paperless-ngx/paperless-ngx/pull/4778))
- Fix: Alpha layer removal could allow duplicates [@stumpylog](https://github.com/stumpylog) ([#4781](https://github.com/paperless-ngx/paperless-ngx/pull/4781))
- Feature: update user profile [@shamoon](https://github.com/shamoon) ([#4678](https://github.com/paperless-ngx/paperless-ngx/pull/4678))
- Fix: update checker broke in v2.0.0 [@shamoon](https://github.com/shamoon) ([#4773](https://github.com/paperless-ngx/paperless-ngx/pull/4773))
- Fix: only show global drag-drop when files included [@shamoon](https://github.com/shamoon) ([#4767](https://github.com/paperless-ngx/paperless-ngx/pull/4767))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4756](https://github.com/paperless-ngx/paperless-ngx/pull/4756))
- Bump the frontend-jest-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4744](https://github.com/paperless-ngx/paperless-ngx/pull/4744))
- Bump [@<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.39.0 to 1.40.1 in /src-ui @dependabot) ([#4749](https://github.com/paperless-ngx/paperless-ngx/pull/4749))
- Bump wait-on from 7.0.1 to 7.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#4747](https://github.com/paperless-ngx/paperless-ngx/pull/4747))
- Bump [@<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.8.10 to 20.10.2 in /src-ui @dependabot) ([#4748](https://github.com/paperless-ngx/paperless-ngx/pull/4748))
- Enhancement: auto-refresh logs \& tasks [@shamoon](https://github.com/shamoon) ([#4680](https://github.com/paperless-ngx/paperless-ngx/pull/4680))
</details>
## paperless-ngx 2.0.1
### Please Note
Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumption templates or custom fields, we recommend users upgrade to at least v2.1.
### Bug Fixes
- Fix: Increase field the length for consumption template source [@stumpylog](https://github.com/stumpylog) ([#4719](https://github.com/paperless-ngx/paperless-ngx/pull/4719))
- Fix: Set RGB color conversion strategy for PDF outputs [@stumpylog](https://github.com/stumpylog) ([#4709](https://github.com/paperless-ngx/paperless-ngx/pull/4709))
- Fix: Add a warning about a low image DPI which may cause OCR to fail [@stumpylog](https://github.com/stumpylog) ([#4708](https://github.com/paperless-ngx/paperless-ngx/pull/4708))
- Fix: share links for URLs containing 'api' incorrect in dropdown [@shamoon](https://github.com/shamoon) ([#4701](https://github.com/paperless-ngx/paperless-ngx/pull/4701))
### All App Changes
<details>
<summary>4 changes</summary>
- Fix: Increase field the length for consumption template source [@stumpylog](https://github.com/stumpylog) ([#4719](https://github.com/paperless-ngx/paperless-ngx/pull/4719))
- Fix: Set RGB color conversion strategy for PDF outputs [@stumpylog](https://github.com/stumpylog) ([#4709](https://github.com/paperless-ngx/paperless-ngx/pull/4709))
- Fix: Add a warning about a low image DPI which may cause OCR to fail [@stumpylog](https://github.com/stumpylog) ([#4708](https://github.com/paperless-ngx/paperless-ngx/pull/4708))
- Fix: share links for URLs containing 'api' incorrect in dropdown [@shamoon](https://github.com/shamoon) ([#4701](https://github.com/paperless-ngx/paperless-ngx/pull/4701))
</details>
## paperless-ngx 2.0.0
### Please Note
Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumption templates or custom fields, we recommend users upgrade to at least v2.1.
### Breaking Changes
- Breaking: Rename the environment variable for self-signed email certificates [@stumpylog](https://github.com/stumpylog) ([#4346](https://github.com/paperless-ngx/paperless-ngx/pull/4346))
- Breaking: Drop support for Python 3.8 [@stumpylog](https://github.com/stumpylog) ([#4156](https://github.com/paperless-ngx/paperless-ngx/pull/4156))
- Breaking: Remove ARMv7 building of the Docker image [@stumpylog](https://github.com/stumpylog) ([#3973](https://github.com/paperless-ngx/paperless-ngx/pull/3973))
### Notable Changes
- Feature: consumption templates [@shamoon](https://github.com/shamoon) ([#4196](https://github.com/paperless-ngx/paperless-ngx/pull/4196))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Enhancement: Updates the underlying image to use Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4150](https://github.com/paperless-ngx/paperless-ngx/pull/4150))
### Features
- Feature: compact notifications [@shamoon](https://github.com/shamoon) ([#4545](https://github.com/paperless-ngx/paperless-ngx/pull/4545))
- Chore: Backend bulk updates [@stumpylog](https://github.com/stumpylog) ([#4509](https://github.com/paperless-ngx/paperless-ngx/pull/4509))
- Feature: Hungarian translation [@shamoon](https://github.com/shamoon) ([#4552](https://github.com/paperless-ngx/paperless-ngx/pull/4552))
- Chore: API support for id args for documents \& objects [@shamoon](https://github.com/shamoon) ([#4519](https://github.com/paperless-ngx/paperless-ngx/pull/4519))
- Feature: Add Bulgarian translation [@shamoon](https://github.com/shamoon) ([#4470](https://github.com/paperless-ngx/paperless-ngx/pull/4470))
- Feature: Audit Trail [@nanokatz](https://github.com/nanokatz) ([#4425](https://github.com/paperless-ngx/paperless-ngx/pull/4425))
- Feature: Add ahead of time compression of the static files for x86_64 [@stumpylog](https://github.com/stumpylog) ([#4390](https://github.com/paperless-ngx/paperless-ngx/pull/4390))
- Feature: sort sidebar views [@shamoon](https://github.com/shamoon) ([#4381](https://github.com/paperless-ngx/paperless-ngx/pull/4381))
- Feature: Switches to a new client to handle communication with Gotenberg [@stumpylog](https://github.com/stumpylog) ([#4391](https://github.com/paperless-ngx/paperless-ngx/pull/4391))
- barcode logic: strip non-numeric characters from detected ASN string [@queaker](https://github.com/queaker) ([#4379](https://github.com/paperless-ngx/paperless-ngx/pull/4379))
- Feature: Include more updated base tools in Docker image [@stumpylog](https://github.com/stumpylog) ([#4319](https://github.com/paperless-ngx/paperless-ngx/pull/4319))
- CI: speed-up frontend tests on ci [@shamoon](https://github.com/shamoon) ([#4316](https://github.com/paperless-ngx/paperless-ngx/pull/4316))
- Feature: password reset [@shamoon](https://github.com/shamoon) ([#4289](https://github.com/paperless-ngx/paperless-ngx/pull/4289))
- Enhancement: dashboard improvements, drag-n-drop reorder dashboard views [@shamoon](https://github.com/shamoon) ([#4252](https://github.com/paperless-ngx/paperless-ngx/pull/4252))
- Feature: Updates Django to 4.2.5 [@stumpylog](https://github.com/stumpylog) ([#4278](https://github.com/paperless-ngx/paperless-ngx/pull/4278))
- Enhancement: settings reorganization \& improvements, separate admin section [@shamoon](https://github.com/shamoon) ([#4251](https://github.com/paperless-ngx/paperless-ngx/pull/4251))
- Feature: consumption templates [@shamoon](https://github.com/shamoon) ([#4196](https://github.com/paperless-ngx/paperless-ngx/pull/4196))
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
- Feature: New management command for fuzzy matching document content [@stumpylog](https://github.com/stumpylog) ([#4160](https://github.com/paperless-ngx/paperless-ngx/pull/4160))
- Enhancement: Updates the underlying image to use Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4150](https://github.com/paperless-ngx/paperless-ngx/pull/4150))
- Enhancement: frontend better handle slow backend requests [@shamoon](https://github.com/shamoon) ([#4055](https://github.com/paperless-ngx/paperless-ngx/pull/4055))
- Chore: update docker image \& ci testing node to v18 [@shamoon](https://github.com/shamoon) ([#4149](https://github.com/paperless-ngx/paperless-ngx/pull/4149))
- Enhancement: Improved error notifications [@shamoon](https://github.com/shamoon) ([#4062](https://github.com/paperless-ngx/paperless-ngx/pull/4062))
- Feature: Official support for Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4146](https://github.com/paperless-ngx/paperless-ngx/pull/4146))
- Enhancement: Add Afrikaans, Greek \& Norwegian languages [@shamoon](https://github.com/shamoon) ([#4088](https://github.com/paperless-ngx/paperless-ngx/pull/4088))
- Enhancement: add task id to pre/post consume script as env [@andreheuer](https://github.com/andreheuer) ([#4037](https://github.com/paperless-ngx/paperless-ngx/pull/4037))
- Enhancement: update bootstrap to v5.3.1 for backend static pages [@shamoon](https://github.com/shamoon) ([#4060](https://github.com/paperless-ngx/paperless-ngx/pull/4060))
### Bug Fixes
- Fix: Add missing spaces to help string in [@joouha](https://github.com/joouha) ([#4674](https://github.com/paperless-ngx/paperless-ngx/pull/4674))
- Fix: Typo invalidates precondition for doctype, resulting in Exception [@ArminGruner](https://github.com/ArminGruner) ([#4668](https://github.com/paperless-ngx/paperless-ngx/pull/4668))
- Fix: Miscellaneous visual fixes in v2.0.0-beta.rc1 2 [@shamoon](https://github.com/shamoon) ([#4635](https://github.com/paperless-ngx/paperless-ngx/pull/4635))
- Fix: Delay consumption after MODIFY inotify events [@frozenbrain](https://github.com/frozenbrain) ([#4626](https://github.com/paperless-ngx/paperless-ngx/pull/4626))
- Documentation: Add note that trash dir must exist [@shamoon](https://github.com/shamoon) ([#4608](https://github.com/paperless-ngx/paperless-ngx/pull/4608))
- Fix: Miscellaneous v2.0 visual fixes [@shamoon](https://github.com/shamoon) ([#4576](https://github.com/paperless-ngx/paperless-ngx/pull/4576))
- Fix: Force UTF-8 for exporter manifests and don't allow escaping [@stumpylog](https://github.com/stumpylog) ([#4574](https://github.com/paperless-ngx/paperless-ngx/pull/4574))
- Fix: plain text preview overflows [@shamoon](https://github.com/shamoon) ([#4555](https://github.com/paperless-ngx/paperless-ngx/pull/4555))
- Fix: add permissions for custom fields with migration [@shamoon](https://github.com/shamoon) ([#4513](https://github.com/paperless-ngx/paperless-ngx/pull/4513))
- Fix: visually hidden text breaks delete button wrap [@shamoon](https://github.com/shamoon) ([#4462](https://github.com/paperless-ngx/paperless-ngx/pull/4462))
- Fix: API statistics document_file_type_counts return type [@shamoon](https://github.com/shamoon) ([#4464](https://github.com/paperless-ngx/paperless-ngx/pull/4464))
- Fix: Always return a list for audit log check [@shamoon](https://github.com/shamoon) ([#4463](https://github.com/paperless-ngx/paperless-ngx/pull/4463))
- Fix: Only create a Correspondent if the email matches rule filters [@stumpylog](https://github.com/stumpylog) ([#4431](https://github.com/paperless-ngx/paperless-ngx/pull/4431))
- Fix: Combination of consume template with recursive tagging [@stumpylog](https://github.com/stumpylog) ([#4442](https://github.com/paperless-ngx/paperless-ngx/pull/4442))
- Fix: replace drag drop \& clipboard deps with angular cdk [@shamoon](https://github.com/shamoon) ([#4362](https://github.com/paperless-ngx/paperless-ngx/pull/4362))
- Fix: update document modified time on note creation / deletion [@shamoon](https://github.com/shamoon) ([#4374](https://github.com/paperless-ngx/paperless-ngx/pull/4374))
- Fix: Updates to latest imap_tools which includes fix for the meta charset in HTML content [@stumpylog](https://github.com/stumpylog) ([#4355](https://github.com/paperless-ngx/paperless-ngx/pull/4355))
- Fix: Missing creation of a folder in Docker image [@stumpylog](https://github.com/stumpylog) ([#4347](https://github.com/paperless-ngx/paperless-ngx/pull/4347))
- Fix: Retry Tika parsing when Tika returns HTTP 500 [@stumpylog](https://github.com/stumpylog) ([#4334](https://github.com/paperless-ngx/paperless-ngx/pull/4334))
- Fix: get highest ASN regardless of user [@shamoon](https://github.com/shamoon) ([#4326](https://github.com/paperless-ngx/paperless-ngx/pull/4326))
- Fix: Generate secret key with C locale and increase allowed characters [@stumpylog](https://github.com/stumpylog) ([#4277](https://github.com/paperless-ngx/paperless-ngx/pull/4277))
- Fix: long notes cause visual overflow [@shamoon](https://github.com/shamoon) ([#4287](https://github.com/paperless-ngx/paperless-ngx/pull/4287))
- Fix: Ensures all old connections are closed in certain long lived places [@stumpylog](https://github.com/stumpylog) ([#4265](https://github.com/paperless-ngx/paperless-ngx/pull/4265))
- CI: fix playwright browser version mismatch failures [@shamoon](https://github.com/shamoon) ([#4239](https://github.com/paperless-ngx/paperless-ngx/pull/4239))
- Fix: Set a non-zero polling internal when inotify cannot import [@stumpylog](https://github.com/stumpylog) ([#4230](https://github.com/paperless-ngx/paperless-ngx/pull/4230))
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
- Documentation: Fix fuzzy matching details [@stumpylog](https://github.com/stumpylog) ([#4207](https://github.com/paperless-ngx/paperless-ngx/pull/4207))
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
- Fix: Logs the errors during thumbnail generation [@stumpylog](https://github.com/stumpylog) ([#4171](https://github.com/paperless-ngx/paperless-ngx/pull/4171))
- Fix: remove owner details from saved_views api endpoint [@shamoon](https://github.com/shamoon) ([#4158](https://github.com/paperless-ngx/paperless-ngx/pull/4158))
- Fix: dashboard widget card borders hidden by bkgd color [@shamoon](https://github.com/shamoon) ([#4155](https://github.com/paperless-ngx/paperless-ngx/pull/4155))
- Fix: hide entire add user / group buttons if insufficient permissions [@shamoon](https://github.com/shamoon) ([#4133](https://github.com/paperless-ngx/paperless-ngx/pull/4133))
### Documentation
- Documentation: Update documentation to refer only to Docker Compose v2 command [@stumpylog](https://github.com/stumpylog) ([#4650](https://github.com/paperless-ngx/paperless-ngx/pull/4650))
- Documentation: fix typo, add to features list [@tooomm](https://github.com/tooomm) ([#4624](https://github.com/paperless-ngx/paperless-ngx/pull/4624))
- Documentation: Add note that trash dir must exist [@shamoon](https://github.com/shamoon) ([#4608](https://github.com/paperless-ngx/paperless-ngx/pull/4608))
- Documentation: Structure backup sections more clearly [@quantenProjects](https://github.com/quantenProjects) ([#4559](https://github.com/paperless-ngx/paperless-ngx/pull/4559))
- Documentation: update docs, screenshots ahead of Paperless-ngx v2.0 [@shamoon](https://github.com/shamoon) ([#4542](https://github.com/paperless-ngx/paperless-ngx/pull/4542))
- Chore: Cleanup command arguments and standardize process count handling [@stumpylog](https://github.com/stumpylog) ([#4541](https://github.com/paperless-ngx/paperless-ngx/pull/4541))
- Add section for SELinux troubleshooting [@nachtjasmin](https://github.com/nachtjasmin) ([#4528](https://github.com/paperless-ngx/paperless-ngx/pull/4528))
- Documentation: clarify document_exporter includes settings [@coaxial](https://github.com/coaxial) ([#4533](https://github.com/paperless-ngx/paperless-ngx/pull/4533))
- Change: Install script improvements [@m-GDEV](https://github.com/m-GDEV) ([#4387](https://github.com/paperless-ngx/paperless-ngx/pull/4387))
- Fix: update document modified time on note creation / deletion [@shamoon](https://github.com/shamoon) ([#4374](https://github.com/paperless-ngx/paperless-ngx/pull/4374))
- Fix: correct set owner API location in docs, additional test [@shamoon](https://github.com/shamoon) ([#4366](https://github.com/paperless-ngx/paperless-ngx/pull/4366))
- Documentation: Remove old information about building the Docker image locally [@stumpylog](https://github.com/stumpylog) ([#4354](https://github.com/paperless-ngx/paperless-ngx/pull/4354))
- Documentation enhancement: add direct links for all config vars [@shamoon](https://github.com/shamoon) ([#4237](https://github.com/paperless-ngx/paperless-ngx/pull/4237))
- Documentation: Fix fuzzy matching details [@stumpylog](https://github.com/stumpylog) ([#4207](https://github.com/paperless-ngx/paperless-ngx/pull/4207))
### Maintenance
- Chore: Backend bulk updates [@stumpylog](https://github.com/stumpylog) ([#4509](https://github.com/paperless-ngx/paperless-ngx/pull/4509))
- Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#4476](https://github.com/paperless-ngx/paperless-ngx/pull/4476))
- Feature: Add Bulgarian translation [@shamoon](https://github.com/shamoon) ([#4470](https://github.com/paperless-ngx/paperless-ngx/pull/4470))
- Chore: Stop duplicated action runs against internal PRs [@stumpylog](https://github.com/stumpylog) ([#4430](https://github.com/paperless-ngx/paperless-ngx/pull/4430))
- CI: separate frontend deps install [@shamoon](https://github.com/shamoon) ([#4336](https://github.com/paperless-ngx/paperless-ngx/pull/4336))
- CI: speed-up frontend tests on ci [@shamoon](https://github.com/shamoon) ([#4316](https://github.com/paperless-ngx/paperless-ngx/pull/4316))
- Fix: Generate secret key with C locale and increase allowed characters [@stumpylog](https://github.com/stumpylog) ([#4277](https://github.com/paperless-ngx/paperless-ngx/pull/4277))
- Bump leonsteinhaeuser/project-beta-automations from 2.1.0 to 2.2.1 [@dependabot](https://github.com/dependabot) ([#4281](https://github.com/paperless-ngx/paperless-ngx/pull/4281))
- Chore: Updates dependabot to group more dependencies [@stumpylog](https://github.com/stumpylog) ([#4280](https://github.com/paperless-ngx/paperless-ngx/pull/4280))
- Change: update translation string for tasks dialog [@shamoon](https://github.com/shamoon) ([#4263](https://github.com/paperless-ngx/paperless-ngx/pull/4263))
- CI: fix playwright browser version mismatch failures [@shamoon](https://github.com/shamoon) ([#4239](https://github.com/paperless-ngx/paperless-ngx/pull/4239))
- Bump docker/login-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4221](https://github.com/paperless-ngx/paperless-ngx/pull/4221))
- Bump docker/setup-buildx-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4220](https://github.com/paperless-ngx/paperless-ngx/pull/4220))
- Bump docker/setup-qemu-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4211](https://github.com/paperless-ngx/paperless-ngx/pull/4211))
- Bump stumpylog/image-cleaner-action from 0.2.0 to 0.3.0 [@dependabot](https://github.com/dependabot) ([#4210](https://github.com/paperless-ngx/paperless-ngx/pull/4210))
- Bump docker/metadata-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4209](https://github.com/paperless-ngx/paperless-ngx/pull/4209))
- Bump docker/build-push-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4212](https://github.com/paperless-ngx/paperless-ngx/pull/4212))
- Bump actions/checkout from 3 to 4 [@dependabot](https://github.com/dependabot) ([#4208](https://github.com/paperless-ngx/paperless-ngx/pull/4208))
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
### Dependencies
<details>
<summary>39 changes</summary>
- Chore: Bulk update of Python dependencies [@stumpylog](https://github.com/stumpylog) ([#4688](https://github.com/paperless-ngx/paperless-ngx/pull/4688))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4479](https://github.com/paperless-ngx/paperless-ngx/pull/4479))
- Bump [@<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot) ([#4480](https://github.com/paperless-ngx/paperless-ngx/pull/4480))
- Bump concurrently from 8.2.1 to 8.2.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4481](https://github.com/paperless-ngx/paperless-ngx/pull/4481))
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#4478](https://github.com/paperless-ngx/paperless-ngx/pull/4478))
- Bump the frontend-angular-dependencies group in /src-ui with 14 updates [@dependabot](https://github.com/dependabot) ([#4477](https://github.com/paperless-ngx/paperless-ngx/pull/4477))
- Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#4476](https://github.com/paperless-ngx/paperless-ngx/pull/4476))
- Bump [@<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot](https://github.com/<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot) ([#4389](https://github.com/paperless-ngx/paperless-ngx/pull/4389))
- Fix: replace drag drop \& clipboard deps with angular cdk [@shamoon](https://github.com/shamoon) ([#4362](https://github.com/paperless-ngx/paperless-ngx/pull/4362))
- Bump postcss from 8.4.12 to 8.4.31 in /src/paperless_mail/templates [@dependabot](https://github.com/dependabot) ([#4318](https://github.com/paperless-ngx/paperless-ngx/pull/4318))
- Bump [@<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot) ([#4303](https://github.com/paperless-ngx/paperless-ngx/pull/4303))
- Bump the frontend-angular-dependencies group in /src-ui with 8 updates [@dependabot](https://github.com/dependabot) ([#4302](https://github.com/paperless-ngx/paperless-ngx/pull/4302))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4283](https://github.com/paperless-ngx/paperless-ngx/pull/4283))
- Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#4282](https://github.com/paperless-ngx/paperless-ngx/pull/4282))
- Bump [@<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot) ([#4284](https://github.com/paperless-ngx/paperless-ngx/pull/4284))
- Bump leonsteinhaeuser/project-beta-automations from 2.1.0 to 2.2.1 [@dependabot](https://github.com/dependabot) ([#4281](https://github.com/paperless-ngx/paperless-ngx/pull/4281))
- Bump zone.js from 0.13.1 to 0.13.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#4223](https://github.com/paperless-ngx/paperless-ngx/pull/4223))
- Bump [@<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot) ([#4224](https://github.com/paperless-ngx/paperless-ngx/pull/4224))
- Bump the frontend-angular-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4222](https://github.com/paperless-ngx/paperless-ngx/pull/4222))
- Bump docker/login-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4221](https://github.com/paperless-ngx/paperless-ngx/pull/4221))
- Bump docker/setup-buildx-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4220](https://github.com/paperless-ngx/paperless-ngx/pull/4220))
- Bump docker/setup-qemu-action from 2 to 3 [@dependabot](https://github.com/dependabot) ([#4211](https://github.com/paperless-ngx/paperless-ngx/pull/4211))
- Bump bootstrap from 5.3.1 to 5.3.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4217](https://github.com/paperless-ngx/paperless-ngx/pull/4217))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4215](https://github.com/paperless-ngx/paperless-ngx/pull/4215))
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4218](https://github.com/paperless-ngx/paperless-ngx/pull/4218))
- Bump stumpylog/image-cleaner-action from 0.2.0 to 0.3.0 [@dependabot](https://github.com/dependabot) ([#4210](https://github.com/paperless-ngx/paperless-ngx/pull/4210))
- Bump docker/metadata-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4209](https://github.com/paperless-ngx/paperless-ngx/pull/4209))
- Bump uuid from 9.0.0 to 9.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4216](https://github.com/paperless-ngx/paperless-ngx/pull/4216))
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#4213](https://github.com/paperless-ngx/paperless-ngx/pull/4213))
- Bump docker/build-push-action from 4 to 5 [@dependabot](https://github.com/dependabot) ([#4212](https://github.com/paperless-ngx/paperless-ngx/pull/4212))
- Bump actions/checkout from 3 to 4 [@dependabot](https://github.com/dependabot) ([#4208](https://github.com/paperless-ngx/paperless-ngx/pull/4208))
- Chore: update docker image \& ci testing node to v18 [@shamoon](https://github.com/shamoon) ([#4149](https://github.com/paperless-ngx/paperless-ngx/pull/4149))
- Chore: Unlock dependencies \& update them all [@stumpylog](https://github.com/stumpylog) ([#4142](https://github.com/paperless-ngx/paperless-ngx/pull/4142))
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4112](https://github.com/paperless-ngx/paperless-ngx/pull/4112))
- Bump tslib from 2.6.1 to 2.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4108](https://github.com/paperless-ngx/paperless-ngx/pull/4108))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4106](https://github.com/paperless-ngx/paperless-ngx/pull/4106))
- Bump concurrently from 8.2.0 to 8.2.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4111](https://github.com/paperless-ngx/paperless-ngx/pull/4111))
- Bump [@<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot) ([#4110](https://github.com/paperless-ngx/paperless-ngx/pull/4110))
- Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#4104](https://github.com/paperless-ngx/paperless-ngx/pull/4104))
</details>
### All App Changes
<details>
<summary>95 changes</summary>
- Fix: Add missing spaces to help string in [@joouha](https://github.com/joouha) ([#4674](https://github.com/paperless-ngx/paperless-ngx/pull/4674))
- Fix: Typo invalidates precondition for doctype, resulting in Exception [@ArminGruner](https://github.com/ArminGruner) ([#4668](https://github.com/paperless-ngx/paperless-ngx/pull/4668))
- Fix: dark mode inconsistencies in v2.0.0 beta.rc1 [@shamoon](https://github.com/shamoon) ([#4669](https://github.com/paperless-ngx/paperless-ngx/pull/4669))
- Fix: dashboard saved view mobile width in v.2.0.0 beta.rc1 [@shamoon](https://github.com/shamoon) ([#4660](https://github.com/paperless-ngx/paperless-ngx/pull/4660))
- Fix: Miscellaneous visual fixes in v2.0.0-beta.rc1 2 [@shamoon](https://github.com/shamoon) ([#4635](https://github.com/paperless-ngx/paperless-ngx/pull/4635))
- Fix: Delay consumption after MODIFY inotify events [@frozenbrain](https://github.com/frozenbrain) ([#4626](https://github.com/paperless-ngx/paperless-ngx/pull/4626))
- Fix: Import of split-manifests can fail [@stumpylog](https://github.com/stumpylog) ([#4623](https://github.com/paperless-ngx/paperless-ngx/pull/4623))
- Fix: sidebar views dont update after creation in v2.0.0-beta.rc1 [@shamoon](https://github.com/shamoon) ([#4619](https://github.com/paperless-ngx/paperless-ngx/pull/4619))
- Fix: Prevent text wrap on consumption template label [@shamoon](https://github.com/shamoon) ([#4616](https://github.com/paperless-ngx/paperless-ngx/pull/4616))
- Fix: increase width of labels in default perms settings [@shamoon](https://github.com/shamoon) ([#4612](https://github.com/paperless-ngx/paperless-ngx/pull/4612))
- Fix: note deletion fails in v2.0.0-beta.rc1 [@shamoon](https://github.com/shamoon) ([#4602](https://github.com/paperless-ngx/paperless-ngx/pull/4602))
- Fix: Handle override lists being None [@stumpylog](https://github.com/stumpylog) ([#4598](https://github.com/paperless-ngx/paperless-ngx/pull/4598))
- Fix: Miscellaneous v2.0 visual fixes [@shamoon](https://github.com/shamoon) ([#4576](https://github.com/paperless-ngx/paperless-ngx/pull/4576))
- Fix: Force UTF-8 for exporter manifests and don't allow escaping [@stumpylog](https://github.com/stumpylog) ([#4574](https://github.com/paperless-ngx/paperless-ngx/pull/4574))
- Feature: compact notifications [@shamoon](https://github.com/shamoon) ([#4545](https://github.com/paperless-ngx/paperless-ngx/pull/4545))
- Chore: Backend bulk updates [@stumpylog](https://github.com/stumpylog) ([#4509](https://github.com/paperless-ngx/paperless-ngx/pull/4509))
- Fix: plain text preview overflows [@shamoon](https://github.com/shamoon) ([#4555](https://github.com/paperless-ngx/paperless-ngx/pull/4555))
- Feature: Hungarian translation [@shamoon](https://github.com/shamoon) ([#4552](https://github.com/paperless-ngx/paperless-ngx/pull/4552))
- Chore: Cleanup command arguments and standardize process count handling [@stumpylog](https://github.com/stumpylog) ([#4541](https://github.com/paperless-ngx/paperless-ngx/pull/4541))
- Chore: API support for id args for documents \& objects [@shamoon](https://github.com/shamoon) ([#4519](https://github.com/paperless-ngx/paperless-ngx/pull/4519))
- Fix: add permissions for custom fields with migration [@shamoon](https://github.com/shamoon) ([#4513](https://github.com/paperless-ngx/paperless-ngx/pull/4513))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4479](https://github.com/paperless-ngx/paperless-ngx/pull/4479))
- Bump [@<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.38.1 to 1.39.0 in /src-ui @dependabot) ([#4480](https://github.com/paperless-ngx/paperless-ngx/pull/4480))
- Bump concurrently from 8.2.1 to 8.2.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4481](https://github.com/paperless-ngx/paperless-ngx/pull/4481))
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#4478](https://github.com/paperless-ngx/paperless-ngx/pull/4478))
- Bump the frontend-angular-dependencies group in /src-ui with 14 updates [@dependabot](https://github.com/dependabot) ([#4477](https://github.com/paperless-ngx/paperless-ngx/pull/4477))
- Fix: visually hidden text breaks delete button wrap [@shamoon](https://github.com/shamoon) ([#4462](https://github.com/paperless-ngx/paperless-ngx/pull/4462))
- Fix: API statistics document_file_type_counts return type [@shamoon](https://github.com/shamoon) ([#4464](https://github.com/paperless-ngx/paperless-ngx/pull/4464))
- Fix: Always return a list for audit log check [@shamoon](https://github.com/shamoon) ([#4463](https://github.com/paperless-ngx/paperless-ngx/pull/4463))
- Feature: Audit Trail [@nanokatz](https://github.com/nanokatz) ([#4425](https://github.com/paperless-ngx/paperless-ngx/pull/4425))
- Fix: Only create a Correspondent if the email matches rule filters [@stumpylog](https://github.com/stumpylog) ([#4431](https://github.com/paperless-ngx/paperless-ngx/pull/4431))
- Fix: Combination of consume template with recursive tagging [@stumpylog](https://github.com/stumpylog) ([#4442](https://github.com/paperless-ngx/paperless-ngx/pull/4442))
- Feature: Add ahead of time compression of the static files for x86_64 [@stumpylog](https://github.com/stumpylog) ([#4390](https://github.com/paperless-ngx/paperless-ngx/pull/4390))
- Feature: sort sidebar views [@shamoon](https://github.com/shamoon) ([#4381](https://github.com/paperless-ngx/paperless-ngx/pull/4381))
- Feature: Switches to a new client to handle communication with Gotenberg [@stumpylog](https://github.com/stumpylog) ([#4391](https://github.com/paperless-ngx/paperless-ngx/pull/4391))
- barcode logic: strip non-numeric characters from detected ASN string [@queaker](https://github.com/queaker) ([#4379](https://github.com/paperless-ngx/paperless-ngx/pull/4379))
- Bump [@<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot](https://github.com/<!---->babel/traverse from 7.22.11 to 7.23.2 in /src-ui @dependabot) ([#4389](https://github.com/paperless-ngx/paperless-ngx/pull/4389))
- Fix: replace drag drop \& clipboard deps with angular cdk [@shamoon](https://github.com/shamoon) ([#4362](https://github.com/paperless-ngx/paperless-ngx/pull/4362))
- Fix: update document modified time on note creation / deletion [@shamoon](https://github.com/shamoon) ([#4374](https://github.com/paperless-ngx/paperless-ngx/pull/4374))
- Fix: correct set owner API location in docs, additional test [@shamoon](https://github.com/shamoon) ([#4366](https://github.com/paperless-ngx/paperless-ngx/pull/4366))
- Fix: get highest ASN regardless of user [@shamoon](https://github.com/shamoon) ([#4326](https://github.com/paperless-ngx/paperless-ngx/pull/4326))
- Bump postcss from 8.4.12 to 8.4.31 in /src/paperless_mail/templates [@dependabot](https://github.com/dependabot) ([#4318](https://github.com/paperless-ngx/paperless-ngx/pull/4318))
- CI: speed-up frontend tests on ci [@shamoon](https://github.com/shamoon) ([#4316](https://github.com/paperless-ngx/paperless-ngx/pull/4316))
- Bump [@<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.7.0 to 20.8.0 in /src-ui @dependabot) ([#4303](https://github.com/paperless-ngx/paperless-ngx/pull/4303))
- Bump the frontend-angular-dependencies group in /src-ui with 8 updates [@dependabot](https://github.com/dependabot) ([#4302](https://github.com/paperless-ngx/paperless-ngx/pull/4302))
- Feature: password reset [@shamoon](https://github.com/shamoon) ([#4289](https://github.com/paperless-ngx/paperless-ngx/pull/4289))
- Enhancement: dashboard improvements, drag-n-drop reorder dashboard views [@shamoon](https://github.com/shamoon) ([#4252](https://github.com/paperless-ngx/paperless-ngx/pull/4252))
- Fix: long notes cause visual overflow [@shamoon](https://github.com/shamoon) ([#4287](https://github.com/paperless-ngx/paperless-ngx/pull/4287))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4283](https://github.com/paperless-ngx/paperless-ngx/pull/4283))
- Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#4282](https://github.com/paperless-ngx/paperless-ngx/pull/4282))
- Bump [@<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.6.3 to 20.7.0 in /src-ui @dependabot) ([#4284](https://github.com/paperless-ngx/paperless-ngx/pull/4284))
- Fix: Ensures all old connections are closed in certain long lived places [@stumpylog](https://github.com/stumpylog) ([#4265](https://github.com/paperless-ngx/paperless-ngx/pull/4265))
- Change: update translation string for tasks dialog [@shamoon](https://github.com/shamoon) ([#4263](https://github.com/paperless-ngx/paperless-ngx/pull/4263))
- Enhancement: settings reorganization \& improvements, separate admin section [@shamoon](https://github.com/shamoon) ([#4251](https://github.com/paperless-ngx/paperless-ngx/pull/4251))
- Chore: Standardizes the imports across all the files and modules [@stumpylog](https://github.com/stumpylog) ([#4248](https://github.com/paperless-ngx/paperless-ngx/pull/4248))
- Feature: consumption templates [@shamoon](https://github.com/shamoon) ([#4196](https://github.com/paperless-ngx/paperless-ngx/pull/4196))
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
- Fix: Set a non-zero polling internal when inotify cannot import [@stumpylog](https://github.com/stumpylog) ([#4230](https://github.com/paperless-ngx/paperless-ngx/pull/4230))
- Bump zone.js from 0.13.1 to 0.13.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#4223](https://github.com/paperless-ngx/paperless-ngx/pull/4223))
- Bump [@<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.5.8 to 20.6.3 in /src-ui @dependabot) ([#4224](https://github.com/paperless-ngx/paperless-ngx/pull/4224))
- Bump the frontend-angular-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#4222](https://github.com/paperless-ngx/paperless-ngx/pull/4222))
- Bump bootstrap from 5.3.1 to 5.3.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4217](https://github.com/paperless-ngx/paperless-ngx/pull/4217))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4215](https://github.com/paperless-ngx/paperless-ngx/pull/4215))
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4218](https://github.com/paperless-ngx/paperless-ngx/pull/4218))
- Bump uuid from 9.0.0 to 9.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4216](https://github.com/paperless-ngx/paperless-ngx/pull/4216))
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#4213](https://github.com/paperless-ngx/paperless-ngx/pull/4213))
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
- Fix: Logs the errors during thumbnail generation [@stumpylog](https://github.com/stumpylog) ([#4171](https://github.com/paperless-ngx/paperless-ngx/pull/4171))
- Feature: New management command for fuzzy matching document content [@stumpylog](https://github.com/stumpylog) ([#4160](https://github.com/paperless-ngx/paperless-ngx/pull/4160))
- Breaking: Drop support for Python 3.8 [@stumpylog](https://github.com/stumpylog) ([#4156](https://github.com/paperless-ngx/paperless-ngx/pull/4156))
- Fix: dashboard widget card borders hidden by bkgd color [@shamoon](https://github.com/shamoon) ([#4155](https://github.com/paperless-ngx/paperless-ngx/pull/4155))
- Enhancement: frontend better handle slow backend requests [@shamoon](https://github.com/shamoon) ([#4055](https://github.com/paperless-ngx/paperless-ngx/pull/4055))
- Chore: Extend the live service utility for handling 503 errors [@stumpylog](https://github.com/stumpylog) ([#4143](https://github.com/paperless-ngx/paperless-ngx/pull/4143))
- Chore: update docker image \& ci testing node to v18 [@shamoon](https://github.com/shamoon) ([#4149](https://github.com/paperless-ngx/paperless-ngx/pull/4149))
- Fix: hide entire add user / group buttons if insufficient permissions [@shamoon](https://github.com/shamoon) ([#4133](https://github.com/paperless-ngx/paperless-ngx/pull/4133))
- Enhancement: Improved error notifications [@shamoon](https://github.com/shamoon) ([#4062](https://github.com/paperless-ngx/paperless-ngx/pull/4062))
- Feature: Official support for Python 3.11 [@stumpylog](https://github.com/stumpylog) ([#4146](https://github.com/paperless-ngx/paperless-ngx/pull/4146))
- Chore: Unlock dependencies \& update them all [@stumpylog](https://github.com/stumpylog) ([#4142](https://github.com/paperless-ngx/paperless-ngx/pull/4142))
- Change: PWA Manifest to Standalone Display [@swoga](https://github.com/swoga) ([#4129](https://github.com/paperless-ngx/paperless-ngx/pull/4129))
- Enhancement: add --id-range for document_retagger [@kamilkosek](https://github.com/kamilkosek) ([#4080](https://github.com/paperless-ngx/paperless-ngx/pull/4080))
- Enhancement: Add Afrikaans, Greek \& Norwegian languages [@shamoon](https://github.com/shamoon) ([#4088](https://github.com/paperless-ngx/paperless-ngx/pull/4088))
- Enhancement: add task id to pre/post consume script as env [@andreheuer](https://github.com/andreheuer) ([#4037](https://github.com/paperless-ngx/paperless-ngx/pull/4037))
- Enhancement: update bootstrap to v5.3.1 for backend static pages [@shamoon](https://github.com/shamoon) ([#4060](https://github.com/paperless-ngx/paperless-ngx/pull/4060))
- Bump the frontend-jest-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#4112](https://github.com/paperless-ngx/paperless-ngx/pull/4112))
- Bump tslib from 2.6.1 to 2.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#4108](https://github.com/paperless-ngx/paperless-ngx/pull/4108))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#4106](https://github.com/paperless-ngx/paperless-ngx/pull/4106))
- Bump concurrently from 8.2.0 to 8.2.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#4111](https://github.com/paperless-ngx/paperless-ngx/pull/4111))
- Bump [@<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.4.5 to 20.5.8 in /src-ui @dependabot) ([#4110](https://github.com/paperless-ngx/paperless-ngx/pull/4110))
- Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#4104](https://github.com/paperless-ngx/paperless-ngx/pull/4104))
</details>
## paperless-ngx 1.17.4
### Bug Fixes

View File

@@ -181,7 +181,7 @@ configure their endpoints, and enable the feature.
Defaults to "<http://localhost:3000>".
If you run paperless on docker, you can add those services to the
Docker Compose file (see the provided
docker-compose file (see the provided
[`docker-compose.sqlite-tika.yml`](https://github.com/paperless-ngx/paperless-ngx/blob/main/docker/compose/docker-compose.sqlite-tika.yml)
file for reference).
@@ -221,8 +221,6 @@ directory.
inside docker, ensure that this path is within a permanent volume
(such as "../media/trash") so it won't get lost on upgrades.
Note that the directory must exist prior to using this setting.
Defaults to empty (i.e. really delete documents).
#### [`PAPERLESS_MEDIA_ROOT=<path>`](#PAPERLESS_MEDIA_ROOT) {#PAPERLESS_MEDIA_ROOT}
@@ -704,20 +702,6 @@ but could result in missing text content.
this value if you are certain your documents are not malicious and
you need the text which was not OCRed
#### [`PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY=<RGB>`](#PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY) {#PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY}
: Controls the Ghostscript color conversion strategy when creating the archive file. This setting
will only be utilized if the output is a version of PDF/A.
Valid options are CMYK, Gray, LeaveColorUnchanged, RGB or UseDeviceIndependentColor.
You can find more on the settings [here](https://ghostscript.readthedocs.io/en/latest/VectorDevices.html#color-conversion-and-management) in the Ghostscript documentation.
!!! warning
Utilizing some of the options may result in errors when creating archive
files from PDFs.
#### [`PAPERLESS_OCR_USER_ARGS=<json>`](#PAPERLESS_OCR_USER_ARGS) {#PAPERLESS_OCR_USER_ARGS}
: OCRmyPDF offers many more options. Use this parameter to specify any
@@ -733,7 +717,7 @@ they use underscores instead of dashes.
Paperless has been tested to work with the OCR options provided
above. There are many options that are incompatible with each other,
so specifying invalid options may prevent paperless from consuming
any documents. Use with caution!
any documents.
Specify arguments as a JSON dictionary. Keep note of lower case
booleans and double quoted parameter names and strings. Examples:
@@ -1345,10 +1329,6 @@ password. All of these options come from their similarly-named [Django settings]
: Defaults to ''.
#### [`PAPERLESS_EMAIL_FROM=<str>`](#PAPERLESS_EMAIL_FROM) {#PAPERLESS_EMAIL_FROM}
: Defaults to PAPERLESS_EMAIL_HOST_USER if not set.
#### [`PAPERLESS_EMAIL_HOST_PASSWORD=<str>`](#PAPERLESS_EMAIL_HOST_PASSWORD) {#PAPERLESS_EMAIL_HOST_PASSWORD}
: Defaults to ''.

View File

@@ -9,7 +9,7 @@ following way:
- `main` always represents the latest release and will only see
changes when a new release is made.
- `dev` contains the code that will be in the next release.
- `feature-X` contains bigger changes that will be in some release, but
- `feature-X` contain bigger changes that will be in some release, but
not necessarily the next one.
When making functional changes to Paperless-ngx, _always_ make your changes

View File

@@ -83,11 +83,11 @@ has to do much less work to serve the data.
## _How do I install paperless-ngx on Raspberry Pi?_
**A:** Docker images are available for arm64 hardware, so just
follow the [Docker Compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
follow the [docker-compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
space compared to a bare metal installation, docker comes with close to
zero overhead, even on Raspberry Pi.
If you decide to go with the bare metal route, be aware that some of
If you decide to got with the bare metal route, be aware that some of
the python requirements do not have precompiled packages for ARM /
ARM64. Installation of these will require additional development
libraries and compilation will take a long time.

View File

@@ -18,9 +18,7 @@ physical documents into a searchable online archive so you can keep, well, _less
## Features
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
- Performs **OCR** on your documents, adding selectable text to image-only documents.
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents)[^1] and more.
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents.

View File

@@ -25,10 +25,7 @@ necessary configuration files, pull the docker image, start paperless
and create your user account. This script essentially performs all the
steps described in [Docker setup](#docker_hub) automatically.
1. Make sure that Docker and Docker Compose are installed.
!!! tip
See the Docker installation instructions at https://docs.docker.com/engine/install/
1. Make sure that docker and docker-compose are installed.
2. Download and run the installation script:
@@ -65,19 +62,19 @@ steps described in [Docker setup](#docker_hub) automatically.
For new installations, it is recommended to use PostgreSQL as the
database backend.
3. Install [Docker](https://docs.docker.com/engine/install/) and
[Docker Compose](https://docs.docker.com/compose/install/).
3. Install [Docker](https://www.docker.com/) and
[docker-compose](https://docs.docker.com/compose/install/).
!!! warning
If you want to use the included `docker-compose.*.yml` file, you
need to have at least Docker version **17.09.0** and Docker Compose
version **v2**. To check do: `docker compose -v` or `docker -v`
need to have at least Docker version **17.09.0** and docker-compose
version **1.17.0**. To check do: `docker-compose -v` or `docker -v`
See the [Docker installation guide](https://docs.docker.com/engine/install/) on how to install the current
version of Docker for your operating system or Linux distribution of
choice. To get the latest version of Docker Compose, follow the
[Docker Compose installation guide](https://docs.docker.com/compose/install/linux/) if your package repository
choice. To get the latest version of docker-compose, follow the
[docker-compose installation guide](https://docs.docker.com/compose/install/linux/) if your package repository
doesn't include it.
4. Modify `docker-compose.yml` to your preferences. You may want to
@@ -168,13 +165,13 @@ steps described in [Docker setup](#docker_hub) automatically.
[`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See
[here](configuration.md#polling).
6. Run `docker compose pull`. This will pull the image.
6. Run `docker-compose pull`. This will pull the image.
7. To be able to login, you will need a super user. To create it,
execute the following command:
```shell-session
$ docker compose run --rm webserver createsuperuser
$ docker-compose run --rm webserver createsuperuser
```
or using docker exec from within the container:
@@ -186,7 +183,7 @@ steps described in [Docker setup](#docker_hub) automatically.
This will prompt you to set a username, an optional e-mail address
and finally a password (at least 8 characters).
8. Run `docker compose up -d`. This will create and start the necessary containers.
8. Run `docker-compose up -d`. This will create and start the necessary containers.
9. The default `docker-compose.yml` exports the webserver on your local
port
@@ -212,14 +209,14 @@ steps described in [Docker setup](#docker_hub) automatically.
root as well.
3. In the `docker-compose.yml` file, find the line that instructs
Docker Compose to pull the paperless image from Docker Hub:
docker-compose to pull the paperless image from Docker Hub:
```yaml
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
```
and replace it with a line that instructs Docker Compose to build
and replace it with a line that instructs docker-compose to build
the image from the current working directory instead:
```yaml
@@ -229,10 +226,10 @@ steps described in [Docker setup](#docker_hub) automatically.
```
4. Follow steps 3 to 8 of [Docker Setup](#docker_hub). When asked to run
`docker compose pull` to pull the image, do
`docker-compose pull` to pull the image, do
```shell-session
$ docker compose build
$ docker-compose build
```
instead to build the image.
@@ -544,7 +541,7 @@ to
image: ghcr.io/paperless-ngx/paperless-ngx:latest
```
and then run `docker compose up -d` which will pull the new image
and then run `docker-compose up -d` which will pull the new image
recreate the container. That's it!
Users who installed with the bare-metal route should also update their
@@ -573,7 +570,7 @@ installation. The important things to keep in mind are as follows:
- The task scheduler of paperless, which is used to execute periodic
tasks such as email checking and maintenance, requires a
[redis](https://redis.io/) message broker instance. The
Docker Compose route takes care of that.
docker-compose route takes care of that.
- The layout of the folder structure for your documents and data
remains the same, so you can just plug your old docker volumes into
paperless-ngx and expect it to find everything where it should be.
@@ -584,7 +581,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
```bash
$ cd /path/to/current/paperless
$ docker compose down
$ docker-compose down
```
2. Do a backup for two purposes: If something goes wrong, you still
@@ -592,7 +589,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
switch back to paperless.
3. Download the latest release of paperless-ngx. You can either go with
the Docker Compose files from
the docker-compose files from
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
or clone the repository to build the image yourself (see
[above](#docker_build)). You can
@@ -629,7 +626,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
the search index:
```shell-session
$ docker compose run --rm webserver document_index reindex
$ docker-compose run --rm webserver document_index reindex
```
This will migrate your database and create the search index. After
@@ -638,7 +635,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
8. Start paperless-ngx.
```bash
$ docker compose up -d
$ docker-compose up -d
```
This will run paperless in the background and automatically start it
@@ -682,7 +679,7 @@ commands as well.
8. Modify the `image:` to point to
`ghcr.io/paperless-ngx/paperless-ngx:latest` or a specific version
if preferred.
9. Start the containers as before, using `docker compose`.
9. Start the containers as before, using `docker-compose`.
## Moving data from SQLite to PostgreSQL or MySQL/MariaDB {#sqlite_to_psql}
@@ -740,7 +737,7 @@ below use PostgreSQL, but are applicable to MySQL/MariaDB with the
``` shell-session
$ cd /path/to/paperless
$ docker compose run --rm webserver /bin/bash
$ docker-compose run --rm webserver /bin/bash
```
This will launch the container and initialize the PostgreSQL
@@ -793,7 +790,7 @@ Execute this:
```shell-session
$ cd /path/to/paperless
$ docker compose run --rm webserver migrate documents 0023
$ docker-compose run --rm webserver migrate documents 0023
```
Or without docker:

View File

@@ -6,7 +6,7 @@ Check for the following issues:
- Ensure that the directory you're putting your documents in is the
folder paperless is watching. With docker, this setting is performed
in the `docker-compose.yml` file. Without Docker, look at the
in the `docker-compose.yml` file. Without docker, look at the
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
using docker.
@@ -120,7 +120,7 @@ Gotenberg raises this error.
You can increase the timeout by configuring a command flag for Gotenberg
(see also [here](https://gotenberg.dev/docs/modules/api#properties)). If
using Docker Compose, this is achieved by the following configuration
using docker-compose, this is achieved by the following configuration
change in the `docker-compose.yml` file:
```yaml

View File

@@ -279,11 +279,10 @@ Consumption templates allow you to filter by:
Consumption templates can assign:
- Title, see [title placeholders](usage.md#title-placeholders) below
- Title, see [title placeholders](usage.md#title_placeholders) below
- Tags, correspondent, document types
- Document owner
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
### Consumption template permissions
@@ -343,7 +342,6 @@ The following custom field types are supported:
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: float number with exactly two decimals, e.g. 12.30
- `Document Link`: reference(s) to other document(s), displayed as links
## Share Links

View File

@@ -56,9 +56,14 @@ if ! command -v docker &> /dev/null ; then
exit 1
fi
if ! command -v docker compose &> /dev/null ; then
echo "docker compose executable not found. Is docker compose installed?"
exit 1
DOCKER_COMPOSE_CMD="docker-compose"
if ! command -v ${DOCKER_COMPOSE_CMD} &> /dev/null ; then
if docker compose version &> /dev/null ; then
DOCKER_COMPOSE_CMD="docker compose"
else
echo "docker-compose executable not found. Is docker-compose installed?"
exit 1
fi
fi
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
@@ -377,16 +382,16 @@ if [ "$l1" -eq "$l2" ] ; then
fi
docker compose pull
${DOCKER_COMPOSE_CMD} pull
if [ "$DATABASE_BACKEND" == "postgres" ] || [ "$DATABASE_BACKEND" == "mariadb" ] ; then
echo "Starting DB first for initialization"
docker compose up --detach db
echo "Starting DB first for initilzation"
${DOCKER_COMPOSE_CMD} up --detach db
# hopefully enough time for even the slower systems
sleep 15
docker compose stop
${DOCKER_COMPOSE_CMD} stop
fi
docker compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
docker compose up --detach
${DOCKER_COMPOSE_CMD} up --detach

View File

@@ -67,5 +67,4 @@ extra:
- icon: material/chat
link: https://matrix.to/#/#paperless:matrix.org
plugins:
- search
- glightbox

View File

@@ -1,8 +1,7 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*",
"/src/app/components/common/pdf-viewer/**"
"projects/**/*"
],
"overrides": [
{

View File

@@ -65,7 +65,7 @@
"src/assets",
"src/manifest.webmanifest",
{
"glob": "{pdf.worker.min.js,pdf.min.js}",
"glob": "pdf.worker.min.js",
"input": "node_modules/pdfjs-dist/build/",
"output": "/assets/js/"
}
@@ -75,8 +75,7 @@
],
"scripts": [],
"allowedCommonJsDependencies": [
"pdfjs-dist",
"pdfjs-dist/web/pdf_viewer"
"ng2-pdf-viewer"
],
"vendorChunk": true,
"extractLicenses": false,
@@ -110,7 +109,7 @@
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "30kb"
"maximumError": "10kb"
}
]
},

View File

@@ -79,7 +79,7 @@ test('should show a mobile preview', async ({ page }) => {
await page.setViewportSize({ width: 400, height: 1000 })
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
await page.getByRole('tab', { name: 'Preview' }).click()
await page.waitForSelector('pngx-pdf-viewer')
await page.waitForSelector('pdf-viewer')
})
test('should show a list of notes', async ({ page }) => {

View File

@@ -7,7 +7,6 @@ module.exports = {
'abstract-name-filter-service',
'abstract-paperless-service',
],
coveragePathIgnorePatterns: ['/src/app/components/common/pdf-viewer/*'],
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
moduleNameMapper: {
'^src/(.*)': '<rootDir>/src/$1',

File diff suppressed because it is too large Load Diff

588
src-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,11 @@
"bootstrap": "^5.3.2",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.0.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.6",
"pdfjs-dist": "^3.11.174",
"rxjs": "^7.8.1",
"tslib": "^2.6.2",
"uuid": "^9.0.1",
@@ -47,20 +47,20 @@
"@angular-eslint/template-parser": "16.2.0",
"@angular/cli": "~16.2.9",
"@angular/compiler-cli": "~16.2.3",
"@playwright/test": "^1.40.1",
"@types/jest": "^29.5.10",
"@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@playwright/test": "^1.39.0",
"@types/jest": "^29.5.7",
"@types/node": "^20.8.10",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"concurrently": "^8.2.2",
"eslint": "^8.55.0",
"eslint": "^8.52.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^13.1.4",
"jest-preset-angular": "^13.1.1",
"jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0",
"ts-node": "~10.9.1",
"typescript": "^5.1.6",
"wait-on": "^7.2.0"
"wait-on": "^7.0.1"
}
}

View File

@@ -33,6 +33,8 @@ export class AppComponent implements OnInit, OnDestroy {
private renderer: Renderer2,
private permissionsService: PermissionsService
) {
let anyWindow = window as any
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
this.settings.updateAppearanceSettings()
}

View File

@@ -51,6 +51,7 @@ import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-v
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
import { YesNoPipe } from './pipes/yes-no.pipe'
import { FileSizePipe } from './pipes/file-size.pipe'
@@ -104,9 +105,6 @@ import { FileDropComponent } from './components/file-drop/file-drop.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component'
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar'
@@ -258,9 +256,6 @@ function initializeApp(settings: SettingsService) {
CustomFieldsComponent,
CustomFieldEditDialogComponent,
CustomFieldsDropdownComponent,
ProfileEditDialogComponent,
PdfViewerComponent,
DocumentLinkComponent,
],
imports: [
BrowserModule,
@@ -270,6 +265,7 @@ function initializeApp(settings: SettingsService) {
FormsModule,
ReactiveFormsModule,
NgxFileDropModule,
PdfViewerModule,
NgSelectModule,
ColorSliderModule,
TourNgBootstrapModule,

View File

@@ -1,19 +1,14 @@
<pngx-page-header title="Logs" i18n-title>
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</pngx-page-header>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
<li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile">
<a ngbNavLink>
{{logFile}}.log
</a>
<a ngbNavLink>{{logFile}}.log</a>
</li>
<div *ngIf="isLoading || !logFiles.length" class="ps-2 d-flex align-items-center">
<div *ngIf="isLoading && !logFiles.length" class="pb-2">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container *ngIf="!logFiles.length" i18n>Loading...</ng-container>
<ng-container i18n>Loading...</ng-container>
</div>
</ul>

View File

@@ -1,9 +1,4 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LogsComponent } from './logs.component'
@@ -31,7 +26,6 @@ describe('LogsComponent', () => {
let fixture: ComponentFixture<LogsComponent>
let logService: LogService
let logSpy
let reloadSpy
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -48,9 +42,7 @@ describe('LogsComponent', () => {
})
fixture = TestBed.createComponent(LogsComponent)
component = fixture.componentInstance
reloadSpy = jest.spyOn(component, 'reloadLogs')
window.HTMLElement.prototype.scroll = function () {} // mock scroll
jest.useFakeTimers()
fixture.detectChanges()
})
@@ -76,14 +68,4 @@ describe('LogsComponent', () => {
component.reloadLogs()
expect(component.logs).toHaveLength(0)
})
it('should auto refresh, allow toggle', () => {
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
component.toggleAutoRefresh()
expect(component.autoRefreshInterval).toBeNull()
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
})

View File

@@ -27,8 +27,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
public isLoading: boolean = false
public autoRefreshInterval: any
@ViewChild('logContainer') logContainer: ElementRef
ngOnInit(): void {
@@ -43,7 +41,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
this.activeLog = this.logFiles[0]
this.reloadLogs()
}
this.toggleAutoRefresh()
})
}
@@ -94,15 +91,4 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
behavior: 'auto',
})
}
toggleAutoRefresh(): void {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval)
this.autoRefreshInterval = null
} else {
this.autoRefreshInterval = setInterval(() => {
this.reloadLogs()
}, 5000)
}
}
}

View File

@@ -184,7 +184,7 @@
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Owner</span>
</div>
<div class="col-md-5">
<div class="col-md-4">
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div>
@@ -193,9 +193,9 @@
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default View Permissions</span>
</div>
<div class="col-md-5">
<div class="col-md-4">
<div class="row">
<div class="col-3">
<div class="col-2">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
@@ -205,7 +205,7 @@
</div>
</div>
<div class="row">
<div class="col-3">
<div class="col-2">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
@@ -220,9 +220,9 @@
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Edit Permissions</span>
</div>
<div class="col-md-5">
<div class="col-md-4">
<div class="row">
<div class="col-3">
<div class="col-2">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
@@ -232,7 +232,7 @@
</div>
</div>
<div class="row">
<div class="col-3">
<div class="col-2">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">

View File

@@ -416,7 +416,7 @@ export class SettingsComponent
)
this.settings.set(
SETTINGS_KEYS.THEME_COLOR,
this.settingsForm.value.themeColor
this.settingsForm.value.themeColor.toString()
)
this.settings.set(
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER,

View File

@@ -1,5 +1,5 @@
<pngx-page-header title="File Tasks" i18n-title>
<div class="btn-toolbar col col-md-auto align-items-center">
<div class="btn-toolbar col col-md-auto">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
@@ -10,10 +10,15 @@
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
</svg>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container>
</button>
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
<button class="btn btn-sm btn-outline-primary" (click)="tasksService.reload()">
<svg *ngIf="!tasksService.loading" class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-clockwise"/>
</svg>
<ng-container *ngIf="tasksService.loading">
<div class="spinner-border spinner-border-sm fw-normal" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-container>&nbsp;<ng-container i18n>Refresh</ng-container>
</button>
</div>
</pngx-page-header>

View File

@@ -112,7 +112,6 @@ describe('TasksComponent', () => {
let modalService: NgbModal
let router: Router
let httpTestingController: HttpTestingController
let reloadSpy
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -142,13 +141,11 @@ describe('TasksComponent', () => {
}).compileComponents()
tasksService = TestBed.inject(TasksService)
reloadSpy = jest.spyOn(tasksService, 'reload')
httpTestingController = TestBed.inject(HttpTestingController)
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
fixture = TestBed.createComponent(TasksComponent)
component = fixture.componentInstance
jest.useFakeTimers()
fixture.detectChanges()
httpTestingController
.expectOne(`${environment.apiBaseUrl}tasks/`)
@@ -167,7 +164,7 @@ describe('TasksComponent', () => {
`Failed${currentTasksLength}`
)
expect(
fixture.debugElement.queryAll(By.css('table input[type="checkbox"]'))
fixture.debugElement.queryAll(By.css('input[type="checkbox"]'))
).toHaveLength(currentTasksLength + 1)
currentTasksLength = tasks.filter(
@@ -248,7 +245,7 @@ describe('TasksComponent', () => {
it('should support toggle all tasks', () => {
const toggleCheck = fixture.debugElement.query(
By.css('table input[type=checkbox]')
By.css('input[type=checkbox]')
)
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
@@ -272,15 +269,4 @@ describe('TasksComponent', () => {
tasks[3].related_document,
])
})
it('should auto refresh, allow toggle', () => {
expect(reloadSpy).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(5000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
component.toggleAutoRefresh()
expect(component.autoRefreshInterval).toBeNull()
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
})

View File

@@ -23,8 +23,6 @@ export class TasksComponent
public pageSize: number = 25
public page: number = 1
public autoRefreshInterval: any
get dismissButtonText(): string {
return this.selectedTasks.size > 0
? $localize`Dismiss selected`
@@ -41,7 +39,6 @@ export class TasksComponent
ngOnInit() {
this.tasksService.reload()
this.toggleAutoRefresh()
}
ngOnDestroy() {
@@ -138,15 +135,4 @@ export class TasksComponent
return $localize`failed`
}
}
toggleAutoRefresh(): void {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval)
this.autoRefreshInterval = null
} else {
this.autoRefreshInterval = setInterval(() => {
this.tasksService.reload()
}, 5000)
}
}
}

View File

@@ -89,7 +89,7 @@ export class UsersAndGroupsComponent
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/`
}, 2500)
} else {
this.toastService.showInfo(

View File

@@ -39,11 +39,6 @@
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
<div class="dropdown-divider"></div>
</div>
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg><ng-container i18n>My Profile</ng-container>
</button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
@@ -92,12 +87,12 @@
</li>
</ul>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews?.length > 0'>
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || sidebarViews?.length > 0'>
<span i18n>Saved views</span>
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
</h6>
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews"
<li class="nav-item w-100" *ngFor="let view of sidebarViews"
cdkDrag
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent"

View File

@@ -27,7 +27,7 @@
max-width: 16.66666667%;
}
@media (min-width: 2400px) {
@media (min-width: 2000px) {
max-width: 8.33333333%;
}
@@ -312,7 +312,7 @@ main {
}
}
.nav-item > .position-absolute {
.nav-item .position-absolute {
cursor: move;
}

View File

@@ -9,7 +9,7 @@ import {
fakeAsync,
tick,
} from '@angular/core/testing'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing'
import { SettingsService } from 'src/app/services/settings.service'
@@ -19,7 +19,7 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Observable, of, tap, throwError } from 'rxjs'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
@@ -32,7 +32,6 @@ import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
const saved_views = [
{
@@ -87,7 +86,6 @@ describe('AppFrameComponent', () => {
let documentListViewService: DocumentListViewService
let router: Router
let savedViewSpy
let modalService: NgbModal
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -100,7 +98,6 @@ describe('AppFrameComponent', () => {
FormsModule,
ReactiveFormsModule,
DragDropModule,
NgbModalModule,
],
providers: [
SettingsService,
@@ -123,7 +120,6 @@ describe('AppFrameComponent', () => {
ToastService,
OpenDocumentsService,
SearchService,
NgbModal,
{
provide: ActivatedRoute,
useValue: {
@@ -152,7 +148,6 @@ describe('AppFrameComponent', () => {
openDocumentsService = TestBed.inject(OpenDocumentsService)
searchService = TestBed.inject(SearchService)
documentListViewService = TestBed.inject(DocumentListViewService)
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
jest
@@ -298,21 +293,6 @@ describe('AppFrameComponent', () => {
expect(autocompleteSpy).toHaveBeenCalled()
}))
it('should handle autocomplete backend failure gracefully', fakeAsync(() => {
const serviceAutocompleteSpy = jest.spyOn(searchService, 'autocomplete')
serviceAutocompleteSpy.mockReturnValue(
throwError(() => new Error('autcomplete failed'))
)
// serviceAutocompleteSpy.mockReturnValue(of([' world']))
let result
component.searchAutoComplete(of('hello')).subscribe((res) => {
result = res
})
tick(250)
expect(serviceAutocompleteSpy).toHaveBeenCalled()
expect(result).toEqual([])
}))
it('should support reset search field', () => {
const resetSpy = jest.spyOn(component, 'resetSearchField')
const input = (fixture.nativeElement as HTMLDivElement).querySelector(
@@ -383,12 +363,4 @@ describe('AppFrameComponent', () => {
>)
expect(toastSpy).toHaveBeenCalled()
})
it('should support edit profile', () => {
const modalSpy = jest.spyOn(modalService, 'open')
component.editProfile()
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
backdrop: 'static',
})
})
})

View File

@@ -8,7 +8,6 @@ import {
map,
switchMap,
first,
catchError,
} from 'rxjs/operators'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
@@ -40,8 +39,6 @@ import {
CdkDragDrop,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
@Component({
selector: 'pngx-app-frame',
@@ -61,6 +58,8 @@ export class AppFrameComponent
searchField = new FormControl('')
sidebarViews: PaperlessSavedView[]
constructor(
public router: Router,
private activatedRoute: ActivatedRoute,
@@ -72,7 +71,6 @@ export class AppFrameComponent
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService,
private modalService: NgbModal,
permissionsService: PermissionsService
) {
super()
@@ -92,6 +90,10 @@ export class AppFrameComponent
this.checkForUpdates()
}
this.tasksService.reload()
this.savedViewService.listAll().subscribe(() => {
this.sidebarViews = this.savedViewService.sidebarViews
})
}
toggleSlimSidebar(): void {
@@ -125,13 +127,6 @@ export class AppFrameComponent
this.isMenuCollapsed = true
}
editProfile() {
this.modalService.open(ProfileEditDialogComponent, {
backdrop: 'static',
})
this.closeMenu()
}
get openDocuments(): PaperlessDocument[] {
return this.openDocumentsService.getOpenDocuments()
}
@@ -167,13 +162,7 @@ export class AppFrameComponent
}
}),
switchMap((term) =>
term.length < 2
? from([[]])
: this.searchService.autocomplete(term).pipe(
catchError(() => {
return from([[]])
})
)
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
)
)
@@ -251,10 +240,9 @@ export class AppFrameComponent
}
onDrop(event: CdkDragDrop<PaperlessSavedView[]>) {
const sidebarViews = this.savedViewService.sidebarViews.concat([])
moveItemInArray(sidebarViews, event.previousIndex, event.currentIndex)
moveItemInArray(this.sidebarViews, event.previousIndex, event.currentIndex)
this.settingsService.updateSidebarViewsSort(sidebarViews).subscribe({
this.settingsService.updateSidebarViewsSort(this.sidebarViews).subscribe({
next: () => {
this.toastService.showInfo($localize`Sidebar views updated`)
},

View File

@@ -17,7 +17,7 @@
<div class="col-md-4">
<h5 class="border-bottom pb-2" i18n>Filters</h5>
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.filter_filename"></pngx-input-select>
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
@@ -35,7 +35,6 @@
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
</div>
<div class="col">
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
@@ -44,7 +43,7 @@
<div class="mb-2">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
<label class="form-label d-block my-2" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
@@ -52,7 +51,7 @@
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
<label class="form-label d-block my-2" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
@@ -63,7 +62,7 @@
<div>
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
<label class="form-label d-block my-2" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
@@ -71,7 +70,7 @@
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
<label class="form-label d-block my-2" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>

View File

@@ -20,7 +20,6 @@ import { TagsComponent } from '../../input/tags/tags.component'
import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component'
import { ConsumptionTemplateEditDialogComponent } from './consumption-template-edit-dialog.component'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
describe('ConsumptionTemplateEditDialogComponent', () => {
let component: ConsumptionTemplateEditDialogComponent
@@ -94,15 +93,6 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
}),
},
},
{
provide: CustomFieldsService,
useValue: {
listAll: () =>
of({
results: [],
}),
},
},
],
imports: [
HttpClientTestingModule,

View File

@@ -18,8 +18,6 @@ import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent } from '../edit-dialog.component'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { PaperlessCustomField } from 'src/app/data/paperless-custom-field'
export const DOCUMENT_SOURCE_OPTIONS = [
{
@@ -47,7 +45,6 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
documentTypes: PaperlessDocumentType[]
storagePaths: PaperlessStoragePath[]
mailRules: PaperlessMailRule[]
customFields: PaperlessCustomField[]
constructor(
service: ConsumptionTemplateService,
@@ -57,8 +54,7 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
storagePathService: StoragePathService,
mailRuleService: MailRuleService,
userService: UserService,
settingsService: SettingsService,
customFieldsService: CustomFieldsService
settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
@@ -81,11 +77,6 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
.listAll()
.pipe(first())
.subscribe((result) => (this.mailRules = result.results))
customFieldsService
.listAll()
.pipe(first())
.subscribe((result) => (this.customFields = result.results))
}
getCreateTitle() {
@@ -115,7 +106,6 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
assign_view_groups: new FormControl([]),
assign_change_users: new FormControl([]),
assign_change_groups: new FormControl([]),
assign_custom_fields: new FormControl([]),
})
}

View File

@@ -1,16 +1,16 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@@ -131,7 +131,6 @@ describe('EditDialogComponent', () => {
})
it('should interpolate object permissions', () => {
component.getMatchingAlgorithms() // coverage
component.object = tag
component.dialogMode = EditDialogMode.EDIT
component.ngOnInit()

View File

@@ -58,8 +58,8 @@ export abstract class EditDialogComponent<
objectForm: FormGroup = this.getForm()
ngOnInit(): void {
if (this.object != null && this.dialogMode !== EditDialogMode.CREATE) {
if ((this.object as ObjectWithPermissions).permissions) {
if (this.object != null) {
if (this.object['permissions']) {
this.object['set_permissions'] = this.object['permissions']
}
@@ -69,8 +69,6 @@ export abstract class EditDialogComponent<
}
this.objectForm.patchValue(this.object)
} else {
// e.g. if name was set
this.objectForm.patchValue(this.object)
// defaults from settings
this.objectForm.patchValue({
permissions_form: {

View File

@@ -21,8 +21,7 @@
<pngx-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></pngx-input-text>
<pngx-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></pngx-input-text>
<pngx-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></pngx-input-text>
<pngx-input-text i18n-title title="Filter attachment filename includes" formControlName="filter_attachment_filename_include" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename_include"></pngx-input-text>
<pngx-input-text i18n-title title="Filter attachment filename excluding" formControlName="filter_attachment_filename_exclude" i18n-hint hint="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename_exclude"></pngx-input-text>
<pngx-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></pngx-input-text>
</div>
<div class="col-md-4">
<pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select>

View File

@@ -158,8 +158,7 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
filter_to: new FormControl(null),
filter_subject: new FormControl(null),
filter_body: new FormControl(null),
filter_attachment_filename_include: new FormControl(null),
filter_attachment_filename_exclude: new FormControl(null),
filter_attachment_filename: new FormControl(null),
maximum_age: new FormControl(null),
attachment_type: new FormControl(MailFilterAttachmentType.Attachments),
consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments),

View File

@@ -1,7 +1,7 @@
<div class="mb-3">
<div class="row">
<div *ngIf="horizontal" class="d-flex align-items-center position-relative hidden-button-container col-md-3">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>

View File

@@ -1,7 +1,7 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>

View File

@@ -1,50 +0,0 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
</div>
<div [class.col-md-9]="horizontal">
<div>
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
[disabled]="disabled"
[items]="foundDocuments$ | async"
placeholder="Search for documents"
[notFoundText]="notFoundText"
[multiple]="true"
bindValue="id"
[compareWith]="compareDocuments"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="loading"
[typeahead]="documentsInput$"
(change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center">
<svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
<svg class="sidebaricon-sm me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>{{document.title}}</span>
</a>
</div>
</ng-template>
<ng-template ng-loadingspinner-tmp>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-template>
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
</ng-template>
</ng-select>
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</div>
</div>
</div>

View File

@@ -1,14 +0,0 @@
::ng-deep .ng-select-container .ng-value-container .ng-value {
background-color: transparent !important;
border-color: transparent;
}
.sidebaricon {
cursor: pointer;
}
.badge {
font-size: .75rem;
// --bs-primary: var(--pngx-bg-alt);
// color: var(--pngx-primary-text-contrast);
}

View File

@@ -1,118 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { NgSelectModule } from '@ng-select/ng-select'
import { of, throwError } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { DocumentLinkComponent } from './document-link.component'
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
const documents = [
{
id: 1,
title: 'Document 1 foo',
},
{
id: 12,
title: 'Document 12 bar',
},
{
id: 23,
title: 'Document 23 bar',
},
]
describe('DocumentLinkComponent', () => {
let component: DocumentLinkComponent
let fixture: ComponentFixture<DocumentLinkComponent>
let documentService: DocumentService
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [DocumentLinkComponent],
imports: [
HttpClientTestingModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
],
})
documentService = TestBed.inject(DocumentService)
fixture = TestBed.createComponent(DocumentLinkComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should retrieve selected documents from APIs', () => {
const getSpy = jest.spyOn(documentService, 'getCachedMany')
getSpy.mockImplementation((ids) => {
return of(documents.filter((d) => ids.includes(d.id)))
})
component.writeValue([1])
expect(getSpy).toHaveBeenCalled()
})
it('should search API on select text input', () => {
const listSpy = jest.spyOn(documentService, 'listFiltered')
listSpy.mockImplementation(
(page, pageSize, sortField, sortReverse, filterRules, extraParams) => {
const docs = documents.filter((d) =>
d.title.includes(filterRules[0].value)
)
return of({
count: docs.length,
results: docs,
all: docs.map((d) => d.id),
})
}
)
component.documentsInput$.next('bar')
expect(listSpy).toHaveBeenCalledWith(
1,
null,
'created',
true,
[{ rule_type: FILTER_TITLE, value: 'bar' }],
{ truncate_content: true }
)
listSpy.mockReturnValueOnce(throwError(() => new Error()))
component.documentsInput$.next('foo')
})
it('should load values correctly', () => {
jest.spyOn(documentService, 'getCachedMany').mockImplementation((ids) => {
return of(documents.filter((d) => ids.includes(d.id)))
})
component.writeValue([12, 23])
expect(component.value).toEqual([12, 23])
expect(component.selectedDocuments).toEqual([documents[1], documents[2]])
component.writeValue(null)
expect(component.value).toEqual([])
expect(component.selectedDocuments).toEqual([])
component.writeValue([])
expect(component.value).toEqual([])
expect(component.selectedDocuments).toEqual([])
})
it('should support unselect', () => {
const getSpy = jest.spyOn(documentService, 'getCachedMany')
getSpy.mockImplementation((ids) => {
return of(documents.filter((d) => ids.includes(d.id)))
})
component.writeValue([12, 23])
component.unselect({ id: 23 })
fixture.detectChanges()
expect(component.selectedDocuments).toEqual([documents[1]])
})
it('should use correct compare, trackBy functions', () => {
expect(component.compareDocuments(documents[0], { id: 1 })).toBeTruthy()
expect(component.compareDocuments(documents[0], { id: 2 })).toBeFalsy()
expect(component.trackByFn(documents[1])).toEqual(12)
})
})

View File

@@ -1,120 +0,0 @@
import { Component, forwardRef, OnInit, Input, OnDestroy } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import {
Subject,
Observable,
takeUntil,
concat,
of,
distinctUntilChanged,
tap,
switchMap,
map,
catchError,
} from 'rxjs'
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { DocumentService } from 'src/app/services/rest/document.service'
import { AbstractInputComponent } from '../abstract-input'
@Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DocumentLinkComponent),
multi: true,
},
],
selector: 'pngx-input-document-link',
templateUrl: './document-link.component.html',
styleUrls: ['./document-link.component.scss'],
})
export class DocumentLinkComponent
extends AbstractInputComponent<any[]>
implements OnInit, OnDestroy
{
documentsInput$ = new Subject<string>()
foundDocuments$: Observable<PaperlessDocument[]>
loading = false
selectedDocuments: PaperlessDocument[] = []
private unsubscribeNotifier: Subject<any> = new Subject()
@Input()
notFoundText: string = $localize`No documents found`
constructor(private documentsService: DocumentService) {
super()
}
ngOnInit() {
this.loadDocs()
}
writeValue(documentIDs: number[]): void {
if (!documentIDs || documentIDs.length === 0) {
this.selectedDocuments = []
super.writeValue([])
} else {
this.loading = true
this.documentsService
.getCachedMany(documentIDs)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((documents) => {
this.loading = false
this.selectedDocuments = documents
super.writeValue(documentIDs)
})
}
}
private loadDocs() {
this.foundDocuments$ = concat(
of([]), // default items
this.documentsInput$.pipe(
distinctUntilChanged(),
takeUntil(this.unsubscribeNotifier),
tap(() => (this.loading = true)),
switchMap((title) =>
this.documentsService
.listFiltered(
1,
null,
'created',
true,
[{ rule_type: FILTER_TITLE, value: title }],
{ truncate_content: true }
)
.pipe(
map((results) => results.results),
catchError(() => of([])), // empty on error
tap(() => (this.loading = false))
)
)
)
)
}
unselect(document: PaperlessDocument): void {
this.selectedDocuments = this.selectedDocuments.filter(
(d) => d.id !== document.id
)
this.onChange(this.selectedDocuments.map((d) => d.id))
}
compareDocuments(
document: PaperlessDocument,
selectedDocument: PaperlessDocument
) {
return document.id === selectedDocument.id
}
trackByFn(item: PaperlessDocument) {
return item.id
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
}
}

View File

@@ -1,7 +1,7 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>

View File

@@ -1,15 +1,8 @@
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg>
</button>
</div>
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
</div>

View File

@@ -5,7 +5,6 @@ import {
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { PasswordComponent } from './password.component'
import { By } from '@angular/platform-browser'
describe('PasswordComponent', () => {
let component: PasswordComponent
@@ -34,26 +33,4 @@ describe('PasswordComponent', () => {
// fixture.detectChanges()
// expect(component.value).toEqual('foo')
})
it('should support toggling field visibility', () => {
expect(input.type).toEqual('password')
component.showReveal = true
fixture.detectChanges()
fixture.debugElement.query(By.css('button')).triggerEventHandler('click')
fixture.detectChanges()
expect(input.type).toEqual('text')
})
it('should empty field if password is obfuscated on focus', () => {
component.value = '*********'
component.onFocus()
expect(component.value).toEqual('')
component.onFocusOut()
expect(component.value).toEqual('**********')
})
it('should disable toggle button if no real password', () => {
component.value = '*********'
expect(component.disableRevealToggle).toBeTruthy()
})
})

View File

@@ -1,4 +1,4 @@
import { Component, Input, forwardRef } from '@angular/core'
import { Component, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@@ -15,32 +15,7 @@ import { AbstractInputComponent } from '../abstract-input'
styleUrls: ['./password.component.scss'],
})
export class PasswordComponent extends AbstractInputComponent<string> {
@Input()
showReveal: boolean = false
@Input()
autocomplete: string
public textVisible: boolean = false
public toggleVisibility(): void {
this.textVisible = !this.textVisible
}
public onFocus() {
if (this.value?.replace(/\*/g, '').length === 0) {
this.writeValue('')
}
}
public onFocusOut() {
if (this.value?.length === 0) {
this.writeValue('**********')
this.onChange(this.value)
}
}
get disableRevealToggle(): boolean {
return this.value?.replace(/\*/g, '').length === 0
constructor() {
super()
}
}

View File

@@ -1,7 +1,7 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<label *ngIf="title" class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
@@ -9,7 +9,7 @@
</button>
</div>
<div [class.col-md-9]="horizontal">
<div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
<div [class.input-group]="allowCreateNew || showFilter">
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
[style.color]="textColor"
@@ -42,9 +42,6 @@
</svg>
</button>
</div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0">
<span i18n>Suggestions:</span>&nbsp;

View File

@@ -17,12 +17,3 @@
font-style: italic;
opacity: .75;
}
::ng-deep .is-invalid ng-select .ng-select-container input {
// replicate bootstrap
padding-right: calc(1.5em + 0.75rem) !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important;
background-repeat: no-repeat !important;
background-position: right calc(0.375em + 0.1875rem) center !important;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important;
}

View File

@@ -1,7 +1,7 @@
<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled" [class.pb-3]="getSuggestions().length > 0">
<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled" [class.pb-3]="suggestions">
<div class="row">
<div class="d-flex align-items-center" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" for="tags" i18n>{{title}}</label>
<label class="form-label mb-md-0" for="tags" i18n>{{title}}</label>
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group flex-nowrap">

View File

@@ -32,9 +32,11 @@ import { CheckComponent } from '../check/check.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { TextComponent } from '../text/text.component'
import { ColorComponent } from '../color/color.component'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../select/select.component'
import { SettingsService } from 'src/app/services/settings.service'
import { ColorSliderModule } from 'ngx-color/slider'
import { By } from '@angular/platform-browser'
const tags: PaperlessTag[] = [
{
@@ -61,8 +63,8 @@ const tags: PaperlessTag[] = [
describe('TagsComponent', () => {
let component: TagsComponent
let fixture: ComponentFixture<TagsComponent>
let input: HTMLInputElement
let modalService: NgbModal
let settingsService: SettingsService
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -108,7 +110,6 @@ describe('TagsComponent', () => {
}).compileComponents()
modalService = TestBed.inject(NgbModal)
settingsService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(TagsComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
@@ -138,7 +139,6 @@ describe('TagsComponent', () => {
})
it('should support create new using last search term and open a modal', () => {
settingsService.currentUser = { id: 1 }
let activeInstances: NgbModalRef[]
modalService.activeInstances.subscribe((v) => (activeInstances = v))
component.select.searchTerm = 'foobar'

View File

@@ -1,7 +1,7 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
@@ -9,7 +9,7 @@
</button>
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<div class="invalid-feedback position-absolute top-100">
{{error}}

View File

@@ -1,4 +1,4 @@
import { Component, Input, forwardRef } from '@angular/core'
import { Component, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@@ -15,9 +15,6 @@ import { AbstractInputComponent } from '../abstract-input'
styleUrls: ['./text.component.scss'],
})
export class TextComponent extends AbstractInputComponent<string> {
@Input()
autocomplete: string
constructor() {
super()
}

View File

@@ -1,7 +1,7 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<label class="form-label mb-md-0" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>

View File

@@ -1,4 +1,4 @@
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.height]="height">
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
<g class="text" style="fill:#000">
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -24,13 +24,13 @@ describe('LogoComponent', () => {
})
it('should support setting height', () => {
expect(fixture.debugElement.query(By.css('svg')).attributes.style).toEqual(
'height:6em'
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
'6em'
)
component.height = '10em'
fixture.detectChanges()
expect(fixture.debugElement.query(By.css('svg')).attributes.style).toEqual(
'height:10em'
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
'10em'
)
})
})

View File

@@ -1,3 +0,0 @@
<div #pdfViewerContainer class="pngx-pdf-viewer-container">
<div class="pdfViewer"></div>
</div>

View File

@@ -1,599 +0,0 @@
/**
* This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/pdf-viewer.component.ts
* Created by vadimdez on 21/06/16.
*/
import {
Component,
Input,
Output,
ElementRef,
EventEmitter,
OnChanges,
SimpleChanges,
OnInit,
OnDestroy,
ViewChild,
AfterViewChecked,
NgZone,
} from '@angular/core'
import { from, fromEvent, Subject } from 'rxjs'
import { debounceTime, filter, takeUntil } from 'rxjs/operators'
import * as PDFJS from 'pdfjs-dist'
import * as PDFJSViewer from 'pdfjs-dist/web/pdf_viewer'
import { createEventBus } from './utils/event-bus-utils'
import type {
PDFSource,
PDFPageProxy,
PDFProgressData,
PDFDocumentProxy,
PDFDocumentLoadingTask,
PDFViewerOptions,
ZoomScale,
} from './typings'
import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer'
PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS
// Yea this is a straight hack
declare global {
interface WeakKeyTypes {
symbol: Object
}
type WeakKey = WeakKeyTypes[keyof WeakKeyTypes]
}
export enum RenderTextMode {
DISABLED,
ENABLED,
ENHANCED,
}
@Component({
selector: 'pngx-pdf-viewer',
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent
implements OnChanges, OnInit, OnDestroy, AfterViewChecked
{
static CSS_UNITS = 96.0 / 72.0
static BORDER_WIDTH = 9
@ViewChild('pdfViewerContainer')
pdfViewerContainer!: ElementRef<HTMLDivElement>
public eventBus!: PDFJSViewer.EventBus
public pdfLinkService!: PDFJSViewer.PDFLinkService
public pdfViewer!: PDFJSViewer.PDFViewer | PDFSinglePageViewer
private isVisible = false
private _cMapsUrl =
typeof PDFJS !== 'undefined'
? `https://unpkg.com/pdfjs-dist@${(PDFJS as any).version}/cmaps/`
: null
private _imageResourcesPath =
typeof PDFJS !== 'undefined'
? `https://unpkg.com/pdfjs-dist@${(PDFJS as any).version}/web/images/`
: undefined
private _renderText = true
private _renderTextMode: RenderTextMode = RenderTextMode.ENABLED
private _stickToPage = false
private _originalSize = true
private _pdf: PDFDocumentProxy | undefined
private _page = 1
private _zoom = 1
private _zoomScale: ZoomScale = 'page-width'
private _rotation = 0
private _showAll = true
private _canAutoResize = true
private _fitToPage = false
private _externalLinkTarget = 'blank'
private _showBorders = false
private lastLoaded!: string | Uint8Array | PDFSource | null
private _latestScrolledPage!: number
private resizeTimeout: number | null = null
private pageScrollTimeout: number | null = null
private isInitialized = false
private loadingTask?: PDFDocumentLoadingTask | null
private destroy$ = new Subject<void>()
@Output('after-load-complete') afterLoadComplete =
new EventEmitter<PDFDocumentProxy>()
@Output('page-rendered') pageRendered = new EventEmitter<CustomEvent>()
@Output('pages-initialized') pageInitialized = new EventEmitter<CustomEvent>()
@Output('text-layer-rendered') textLayerRendered =
new EventEmitter<CustomEvent>()
@Output('error') onError = new EventEmitter<any>()
@Output('on-progress') onProgress = new EventEmitter<PDFProgressData>()
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>(true)
@Input() src?: string | Uint8Array | PDFSource
@Input('c-maps-url')
set cMapsUrl(cMapsUrl: string) {
this._cMapsUrl = cMapsUrl
}
@Input('page')
set page(_page: number | string | any) {
_page = parseInt(_page, 10) || 1
const originalPage = _page
if (this._pdf) {
_page = this.getValidPageNumber(_page)
}
this._page = _page
if (originalPage !== _page) {
this.pageChange.emit(_page)
}
}
@Input('render-text')
set renderText(renderText: boolean) {
this._renderText = renderText
}
@Input('render-text-mode')
set renderTextMode(renderTextMode: RenderTextMode) {
this._renderTextMode = renderTextMode
}
@Input('original-size')
set originalSize(originalSize: boolean) {
this._originalSize = originalSize
}
@Input('show-all')
set showAll(value: boolean) {
this._showAll = value
}
@Input('stick-to-page')
set stickToPage(value: boolean) {
this._stickToPage = value
}
@Input('zoom')
set zoom(value: number) {
if (value <= 0) {
return
}
this._zoom = value
}
get zoom() {
return this._zoom
}
@Input('zoom-scale')
set zoomScale(value: ZoomScale) {
this._zoomScale = value
}
get zoomScale() {
return this._zoomScale
}
@Input('rotation')
set rotation(value: number) {
if (!(typeof value === 'number' && value % 90 === 0)) {
console.warn('Invalid pages rotation angle.')
return
}
this._rotation = value
}
@Input('external-link-target')
set externalLinkTarget(value: string) {
this._externalLinkTarget = value
}
@Input('autoresize')
set autoresize(value: boolean) {
this._canAutoResize = Boolean(value)
}
@Input('fit-to-page')
set fitToPage(value: boolean) {
this._fitToPage = Boolean(value)
}
@Input('show-borders')
set showBorders(value: boolean) {
this._showBorders = Boolean(value)
}
static getLinkTarget(type: string) {
switch (type) {
case 'blank':
return (PDFJSViewer as any).LinkTarget.BLANK
case 'none':
return (PDFJSViewer as any).LinkTarget.NONE
case 'self':
return (PDFJSViewer as any).LinkTarget.SELF
case 'parent':
return (PDFJSViewer as any).LinkTarget.PARENT
case 'top':
return (PDFJSViewer as any).LinkTarget.TOP
}
return null
}
constructor(
private element: ElementRef<HTMLElement>,
private ngZone: NgZone
) {
PDFJS.GlobalWorkerOptions['workerSrc'] = 'assets/js/pdf.worker.min.js'
}
ngAfterViewChecked(): void {
if (this.isInitialized) {
return
}
const offset = this.pdfViewerContainer.nativeElement.offsetParent
if (this.isVisible === true && offset == null) {
this.isVisible = false
return
}
if (this.isVisible === false && offset != null) {
this.isVisible = true
setTimeout(() => {
this.initialize()
this.ngOnChanges({ src: this.src } as any)
})
}
}
ngOnInit() {
this.initialize()
this.setupResizeListener()
}
ngOnDestroy() {
this.clear()
this.destroy$.next()
this.loadingTask = null
}
ngOnChanges(changes: SimpleChanges) {
if (!this.isVisible) {
return
}
if ('src' in changes) {
this.loadPDF()
} else if (this._pdf) {
if ('renderText' in changes || 'showAll' in changes) {
this.setupViewer()
this.resetPdfDocument()
}
if ('page' in changes) {
const { page } = changes
if (page.currentValue === this._latestScrolledPage) {
return
}
// New form of page changing: The viewer will now jump to the specified page when it is changed.
// This behavior is introduced by using the PDFSinglePageViewer
this.pdfViewer.scrollPageIntoView({ pageNumber: this._page })
}
this.update()
}
}
public updateSize() {
from(
this._pdf!.getPage(
this.pdfViewer.currentPageNumber
) as unknown as Promise<PDFPageProxy>
)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (page: PDFPageProxy) => {
const rotation = this._rotation + page.rotate
const viewportWidth =
(page as any).getViewport({
scale: this._zoom,
rotation,
}).width * PdfViewerComponent.CSS_UNITS
let scale = this._zoom
let stickToPage = true
// Scale the document when it shouldn't be in original size or doesn't fit into the viewport
if (
!this._originalSize ||
(this._fitToPage &&
viewportWidth > this.pdfViewerContainer.nativeElement.clientWidth)
) {
const viewPort = (page as any).getViewport({ scale: 1, rotation })
scale = this.getScale(viewPort.width, viewPort.height)
stickToPage = !this._stickToPage
}
setTimeout(() => {
this.pdfViewer.currentScale = scale
})
},
})
}
public clear() {
if (this.loadingTask && !this.loadingTask.destroyed) {
this.loadingTask.destroy()
}
if (this._pdf) {
this._latestScrolledPage = 0
this._pdf.destroy()
this._pdf = undefined
}
}
private getPDFLinkServiceConfig() {
const linkTarget = PdfViewerComponent.getLinkTarget(
this._externalLinkTarget
)
if (linkTarget) {
return { externalLinkTarget: linkTarget }
}
return {}
}
private initEventBus() {
this.eventBus = createEventBus(PDFJSViewer, this.destroy$)
fromEvent<CustomEvent>(this.eventBus, 'pagerendered')
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
this.pageRendered.emit(event)
})
fromEvent<CustomEvent>(this.eventBus, 'pagesinit')
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
this.pageInitialized.emit(event)
})
fromEvent(this.eventBus, 'pagechanging')
.pipe(takeUntil(this.destroy$))
.subscribe(({ pageNumber }: any) => {
if (this.pageScrollTimeout) {
clearTimeout(this.pageScrollTimeout)
}
this.pageScrollTimeout = window.setTimeout(() => {
this._latestScrolledPage = pageNumber
this.pageChange.emit(pageNumber)
}, 100)
})
fromEvent<CustomEvent>(this.eventBus, 'textlayerrendered')
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
this.textLayerRendered.emit(event)
})
}
private initPDFServices() {
this.pdfLinkService = new PDFJSViewer.PDFLinkService({
eventBus: this.eventBus,
...this.getPDFLinkServiceConfig(),
})
}
private getPDFOptions(): PDFViewerOptions {
return {
eventBus: this.eventBus,
container: this.element.nativeElement.querySelector('div')!,
removePageBorders: !this._showBorders,
linkService: this.pdfLinkService,
textLayerMode: this._renderText
? this._renderTextMode
: RenderTextMode.DISABLED,
imageResourcesPath: this._imageResourcesPath,
}
}
private setupViewer() {
PDFJS['disableTextLayer'] = !this._renderText
this.initPDFServices()
if (this._showAll) {
this.pdfViewer = new PDFJSViewer.PDFViewer(this.getPDFOptions())
} else {
this.pdfViewer = new PDFJSViewer.PDFSinglePageViewer(this.getPDFOptions())
}
this.pdfLinkService.setViewer(this.pdfViewer)
this.pdfViewer._currentPageNumber = this._page
}
private getValidPageNumber(page: number): number {
if (page < 1) {
return 1
}
if (page > this._pdf!.numPages) {
return this._pdf!.numPages
}
return page
}
private getDocumentParams() {
const srcType = typeof this.src
if (!this._cMapsUrl) {
return this.src
}
const params: any = {
cMapUrl: this._cMapsUrl,
cMapPacked: true,
enableXfa: true,
}
if (srcType === 'string') {
params.url = this.src
} else if (srcType === 'object') {
if ((this.src as any).byteLength !== undefined) {
params.data = this.src
} else {
Object.assign(params, this.src)
}
}
return params
}
private loadPDF() {
if (!this.src) {
return
}
if (this.lastLoaded === this.src) {
this.update()
return
}
this.clear()
this.setupViewer()
this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
this.onProgress.emit(progressData)
}
const src = this.src
from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (pdf) => {
this._pdf = pdf
this.lastLoaded = src
this.afterLoadComplete.emit(pdf)
this.resetPdfDocument()
this.update()
},
error: (error) => {
this.lastLoaded = null
this.onError.emit(error)
},
})
}
private update() {
this.page = this._page
this.render()
}
private render() {
this._page = this.getValidPageNumber(this._page)
if (
this._rotation !== 0 ||
this.pdfViewer.pagesRotation !== this._rotation
) {
setTimeout(() => {
this.pdfViewer.pagesRotation = this._rotation
})
}
if (this._stickToPage) {
setTimeout(() => {
this.pdfViewer.currentPageNumber = this._page
})
}
this.updateSize()
}
private getScale(viewportWidth: number, viewportHeight: number) {
const borderSize = this._showBorders
? 2 * PdfViewerComponent.BORDER_WIDTH
: 0
const pdfContainerWidth =
this.pdfViewerContainer.nativeElement.clientWidth - borderSize
const pdfContainerHeight =
this.pdfViewerContainer.nativeElement.clientHeight - borderSize
if (
pdfContainerHeight === 0 ||
viewportHeight === 0 ||
pdfContainerWidth === 0 ||
viewportWidth === 0
) {
return 1
}
let ratio = 1
switch (this._zoomScale) {
case 'page-fit':
ratio = Math.min(
pdfContainerHeight / viewportHeight,
pdfContainerWidth / viewportWidth
)
break
case 'page-height':
ratio = pdfContainerHeight / viewportHeight
break
case 'page-width':
default:
ratio = pdfContainerWidth / viewportWidth
break
}
return (this._zoom * ratio) / PdfViewerComponent.CSS_UNITS
}
private resetPdfDocument() {
this.pdfLinkService.setDocument(this._pdf, null)
this.pdfViewer.setDocument(this._pdf!)
}
private initialize(): void {
if (!this.isVisible) {
return
}
this.isInitialized = true
this.initEventBus()
this.setupViewer()
}
private setupResizeListener(): void {
this.ngZone.runOutsideAngular(() => {
fromEvent(window, 'resize')
.pipe(
debounceTime(100),
filter(() => this._canAutoResize && !!this._pdf),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.updateSize()
})
})
}
}

View File

@@ -1,17 +0,0 @@
export type PDFPageProxy =
import('pdfjs-dist/types/src/display/api').PDFPageProxy
export type PDFSource =
import('pdfjs-dist/types/src/display/api').DocumentInitParameters
export type PDFDocumentProxy =
import('pdfjs-dist/types/src/display/api').PDFDocumentProxy
export type PDFDocumentLoadingTask =
import('pdfjs-dist/types/src/display/api').PDFDocumentLoadingTask
export type PDFViewerOptions =
import('pdfjs-dist/types/web/pdf_viewer').PDFViewerOptions
export interface PDFProgressData {
loaded: number
total: number
}
export type ZoomScale = 'page-height' | 'page-fit' | 'page-width'

View File

@@ -1,182 +0,0 @@
/**
* This file is taken and modified from https://github.com/VadimDez/ng2-pdf-viewer/blob/10.0.0/src/app/pdf-viewer/utils/event-bus-utils.ts
* Created by vadimdez on 21/06/16.
*/
import { fromEvent, Subject } from 'rxjs'
import { takeUntil } from 'rxjs/operators'
import type { EventBus } from 'pdfjs-dist/web/pdf_viewer'
// interface EventBus {
// on(eventName: string, listener: Function): void;
// off(eventName: string, listener: Function): void;
// _listeners: any;
// dispatch(eventName: string, data: Object): void;
// _on(eventName: any, listener: any, options?: null): void;
// _off(eventName: any, listener: any, options?: null): void;
// }
export function createEventBus(pdfJsViewer: any, destroy$: Subject<void>) {
const globalEventBus: EventBus = new pdfJsViewer.EventBus()
attachDOMEventsToEventBus(globalEventBus, destroy$)
return globalEventBus
}
function attachDOMEventsToEventBus(
eventBus: EventBus,
destroy$: Subject<void>
): void {
fromEvent(eventBus, 'documentload')
.pipe(takeUntil(destroy$))
.subscribe(() => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('documentload', true, true, {})
window.dispatchEvent(event)
})
fromEvent(eventBus, 'pagerendered')
.pipe(takeUntil(destroy$))
.subscribe(({ pageNumber, cssTransform, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('pagerendered', true, true, {
pageNumber,
cssTransform,
})
source.div.dispatchEvent(event)
})
fromEvent(eventBus, 'textlayerrendered')
.pipe(takeUntil(destroy$))
.subscribe(({ pageNumber, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('textlayerrendered', true, true, { pageNumber })
source.textLayerDiv?.dispatchEvent(event)
})
fromEvent(eventBus, 'pagechanging')
.pipe(takeUntil(destroy$))
.subscribe(({ pageNumber, source }: any) => {
const event = document.createEvent('UIEvents') as any
event.initEvent('pagechanging', true, true)
/* tslint:disable:no-string-literal */
event['pageNumber'] = pageNumber
source.container.dispatchEvent(event)
})
fromEvent(eventBus, 'pagesinit')
.pipe(takeUntil(destroy$))
.subscribe(({ source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('pagesinit', true, true, null)
source.container.dispatchEvent(event)
})
fromEvent(eventBus, 'pagesloaded')
.pipe(takeUntil(destroy$))
.subscribe(({ pagesCount, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('pagesloaded', true, true, { pagesCount })
source.container.dispatchEvent(event)
})
fromEvent(eventBus, 'scalechange')
.pipe(takeUntil(destroy$))
.subscribe(({ scale, presetValue, source }: any) => {
const event = document.createEvent('UIEvents') as any
event.initEvent('scalechange', true, true)
/* tslint:disable:no-string-literal */
event['scale'] = scale
/* tslint:disable:no-string-literal */
event['presetValue'] = presetValue
source.container.dispatchEvent(event)
})
fromEvent(eventBus, 'updateviewarea')
.pipe(takeUntil(destroy$))
.subscribe(({ location, source }: any) => {
const event = document.createEvent('UIEvents') as any
event.initEvent('updateviewarea', true, true)
event['location'] = location
source.container.dispatchEvent(event)
})
fromEvent(eventBus, 'find')
.pipe(takeUntil(destroy$))
.subscribe(
({
source,
type,
query,
phraseSearch,
caseSensitive,
highlightAll,
findPrevious,
}: any) => {
if (source === window) {
return // event comes from FirefoxCom, no need to replicate
}
const event = document.createEvent('CustomEvent')
event.initCustomEvent('find' + type, true, true, {
query,
phraseSearch,
caseSensitive,
highlightAll,
findPrevious,
})
window.dispatchEvent(event)
}
)
fromEvent(eventBus, 'attachmentsloaded')
.pipe(takeUntil(destroy$))
.subscribe(({ attachmentsCount, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('attachmentsloaded', true, true, {
attachmentsCount,
})
source.container.dispatchEvent(event)
})
fromEvent(eventBus, 'sidebarviewchanged')
.pipe(takeUntil(destroy$))
.subscribe(({ view, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('sidebarviewchanged', true, true, { view })
source.outerContainer.dispatchEvent(event)
})
fromEvent(eventBus, 'pagemode')
.pipe(takeUntil(destroy$))
.subscribe(({ mode, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('pagemode', true, true, { mode })
source.pdfViewer.container.dispatchEvent(event)
})
fromEvent(eventBus, 'namedaction')
.pipe(takeUntil(destroy$))
.subscribe(({ action, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('namedaction', true, true, { action })
source.pdfViewer.container.dispatchEvent(event)
})
fromEvent(eventBus, 'presentationmodechanged')
.pipe(takeUntil(destroy$))
.subscribe(({ active, switchInProgress }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('presentationmodechanged', true, true, {
active,
switchInProgress,
})
window.dispatchEvent(event)
})
fromEvent(eventBus, 'outlineloaded')
.pipe(takeUntil(destroy$))
.subscribe(({ outlineCount, source }: any) => {
const event = document.createEvent('CustomEvent')
event.initCustomEvent('outlineloaded', true, true, { outlineCount })
source.container.dispatchEvent(event)
})
}

View File

@@ -80,16 +80,7 @@ describe('PermissionsDialogComponent', () => {
it('should return permissions', () => {
expect(component.permissions).toEqual({
owner: null,
set_permissions: {
view: {
users: [],
groups: [],
},
change: {
users: [],
groups: [],
},
},
set_permissions: null,
})
component.form.get('permissions_form').setValue(set_permissions)
expect(component.permissions).toEqual(set_permissions)

View File

@@ -52,17 +52,8 @@ export class PermissionsDialogComponent {
get permissions() {
return {
owner: this.form.get('permissions_form').value?.owner ?? null,
set_permissions: this.form.get('permissions_form').value
?.set_permissions ?? {
view: {
users: [],
groups: [],
},
change: {
users: [],
groups: [],
},
},
set_permissions:
this.form.get('permissions_form').value?.set_permissions ?? null,
}
}

View File

@@ -1,56 +0,0 @@
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
</div>
</div>
</div>
</div>
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
</div>
</div>
</div>
</div>
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
<div class="mb-3">
<label class="form-label" i18n>API Auth Token</label>
<div class="position-relative">
<div class="input-group">
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
</svg>
</button>
</div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
</div>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || saveDisabled">Save</button>
</div>
</form>

View File

@@ -1,9 +0,0 @@
::ng-deep {
.accordion-body .mb-3 {
margin: 0 !important; // hack-ish, for animation
}
}
.copied-badge {
right: 8em;
}

View File

@@ -1,222 +0,0 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
import { ProfileService } from 'src/app/services/profile.service'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import {
NgbAccordionModule,
NgbActiveModal,
NgbModal,
NgbModalModule,
} from '@ng-bootstrap/ng-bootstrap'
import { HttpClientModule } from '@angular/common/http'
import { TextComponent } from '../input/text/text.component'
import { PasswordComponent } from '../input/password/password.component'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { Clipboard } from '@angular/cdk/clipboard'
const profile = {
email: 'foo@bar.com',
password: '*********',
first_name: 'foo',
last_name: 'bar',
auth_token: '123456789abcdef',
}
describe('ProfileEditDialogComponent', () => {
let component: ProfileEditDialogComponent
let fixture: ComponentFixture<ProfileEditDialogComponent>
let profileService: ProfileService
let toastService: ToastService
let clipboard: Clipboard
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
ProfileEditDialogComponent,
TextComponent,
PasswordComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientModule,
ReactiveFormsModule,
FormsModule,
NgbModalModule,
NgbAccordionModule,
],
})
profileService = TestBed.inject(ProfileService)
toastService = TestBed.inject(ToastService)
clipboard = TestBed.inject(Clipboard)
fixture = TestBed.createComponent(ProfileEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should get profile on init, display in form', () => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
expect(getSpy).toHaveBeenCalled()
fixture.detectChanges()
expect(component.form.get('email').value).toEqual(profile.email)
})
it('should update profile on save, display error if needed', () => {
const newProfile = {
email: 'foo@bar2.com',
password: profile.password,
first_name: 'foo2',
last_name: profile.last_name,
auth_token: profile.auth_token,
}
const updateSpy = jest.spyOn(profileService, 'update')
const errorSpy = jest.spyOn(toastService, 'showError')
updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save')))
component.save()
expect(errorSpy).toHaveBeenCalled()
updateSpy.mockClear()
const infoSpy = jest.spyOn(toastService, 'showInfo')
component.form.patchValue(newProfile)
updateSpy.mockReturnValueOnce(of(newProfile))
component.save()
expect(updateSpy).toHaveBeenCalledWith(newProfile)
expect(infoSpy).toHaveBeenCalled()
})
it('should close on cancel', () => {
const closeSpy = jest.spyOn(component.activeModal, 'close')
component.cancel()
expect(closeSpy).toHaveBeenCalled()
})
it('should show additional confirmation field when email changes, warn with error & disable save', () => {
expect(component.form.get('email_confirm').enabled).toBeFalsy()
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
component.form.get('email').patchValue('foo@bar2.com')
component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any)
fixture.detectChanges()
expect(component.form.get('email_confirm').enabled).toBeTruthy()
expect(fixture.debugElement.nativeElement.textContent).toContain(
'Emails must match'
)
expect(component.saveDisabled).toBeTruthy()
component.form.get('email_confirm').patchValue('foo@bar2.com')
component.onEmailConfirmKeyUp({ target: { value: 'foo@bar2.com' } } as any)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Emails must match'
)
expect(component.saveDisabled).toBeFalsy()
component.form.get('email').patchValue(profile.email)
fixture.detectChanges()
expect(component.form.get('email_confirm').enabled).toBeFalsy()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Emails must match'
)
expect(component.saveDisabled).toBeFalsy()
})
it('should show additional confirmation field when password changes, warn with error & disable save', () => {
expect(component.form.get('password_confirm').enabled).toBeFalsy()
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
component.form.get('password').patchValue('new*pass')
component.onPasswordKeyUp({
target: { value: 'new*pass', tagName: 'input' },
} as any)
component.onPasswordKeyUp({ target: { tagName: 'button' } } as any) // coverage
fixture.detectChanges()
expect(component.form.get('password_confirm').enabled).toBeTruthy()
expect(fixture.debugElement.nativeElement.textContent).toContain(
'Passwords must match'
)
expect(component.saveDisabled).toBeTruthy()
component.form.get('password_confirm').patchValue('new*pass')
component.onPasswordConfirmKeyUp({ target: { value: 'new*pass' } } as any)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Passwords must match'
)
expect(component.saveDisabled).toBeFalsy()
component.form.get('password').patchValue(profile.password)
fixture.detectChanges()
expect(component.form.get('password_confirm').enabled).toBeFalsy()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Passwords must match'
)
expect(component.saveDisabled).toBeFalsy()
})
it('should logout on save if password changed', fakeAsync(() => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
component['newPassword'] = 'new*pass'
component.form.get('password').patchValue('new*pass')
component.form.get('password_confirm').patchValue('new*pass')
const updateSpy = jest.spyOn(profileService, 'update')
updateSpy.mockReturnValue(of(null))
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
component.save()
expect(updateSpy).toHaveBeenCalled()
tick(2600)
expect(window.location.href).toContain('logout')
}))
it('should support auth token copy', fakeAsync(() => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
const copySpy = jest.spyOn(clipboard, 'copy')
component.copyAuthToken()
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
expect(component.copied).toBeTruthy()
tick(3000)
expect(component.copied).toBeFalsy()
}))
it('should support generate token, display error if needed', () => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
const generateSpy = jest.spyOn(profileService, 'generateAuthToken')
const errorSpy = jest.spyOn(toastService, 'showError')
generateSpy.mockReturnValueOnce(
throwError(() => new Error('failed to generate'))
)
component.generateAuthToken()
expect(errorSpy).toHaveBeenCalled()
generateSpy.mockClear()
const newToken = '789101112hijk'
generateSpy.mockReturnValueOnce(of(newToken))
component.generateAuthToken()
expect(generateSpy).toHaveBeenCalled()
expect(component.form.get('auth_token').value).not.toEqual(
profile.auth_token
)
expect(component.form.get('auth_token').value).toEqual(newToken)
})
})

View File

@@ -1,184 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { Subject, takeUntil } from 'rxjs'
import { Clipboard } from '@angular/cdk/clipboard'
@Component({
selector: 'pngx-profile-edit-dialog',
templateUrl: './profile-edit-dialog.component.html',
styleUrls: ['./profile-edit-dialog.component.scss'],
})
export class ProfileEditDialogComponent implements OnInit, OnDestroy {
public networkActive: boolean = false
public error: any
private unsubscribeNotifier: Subject<any> = new Subject()
public form = new FormGroup({
email: new FormControl(''),
email_confirm: new FormControl({ value: null, disabled: true }),
password: new FormControl(null),
password_confirm: new FormControl({ value: null, disabled: true }),
first_name: new FormControl(''),
last_name: new FormControl(''),
auth_token: new FormControl(''),
})
private currentPassword: string
private newPassword: string
private passwordConfirm: string
public showPasswordConfirm: boolean = false
private currentEmail: string
private newEmail: string
private emailConfirm: string
public showEmailConfirm: boolean = false
public copied: boolean = false
constructor(
private profileService: ProfileService,
public activeModal: NgbActiveModal,
private toastService: ToastService,
private clipboard: Clipboard
) {}
ngOnInit(): void {
this.networkActive = true
this.profileService
.get()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((profile) => {
this.networkActive = false
this.form.patchValue(profile)
this.currentEmail = profile.email
this.form.get('email').valueChanges.subscribe((newEmail) => {
this.newEmail = newEmail
this.onEmailChange()
})
this.currentPassword = profile.password
this.form.get('password').valueChanges.subscribe((newPassword) => {
this.newPassword = newPassword
this.onPasswordChange()
})
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
}
get saveDisabled(): boolean {
return this.error?.password_confirm || this.error?.email_confirm
}
onEmailKeyUp(event: KeyboardEvent): void {
this.newEmail = (event.target as HTMLInputElement)?.value
this.onEmailChange()
}
onEmailConfirmKeyUp(event: KeyboardEvent): void {
this.emailConfirm = (event.target as HTMLInputElement)?.value
this.onEmailChange()
}
onEmailChange(): void {
this.showEmailConfirm = this.currentEmail !== this.newEmail
if (this.showEmailConfirm) {
this.form.get('email_confirm').enable()
if (this.newEmail !== this.emailConfirm) {
if (!this.error) this.error = {}
this.error.email_confirm = $localize`Emails must match`
} else {
delete this.error?.email_confirm
}
} else {
this.form.get('email_confirm').disable()
delete this.error?.email_confirm
}
}
onPasswordKeyUp(event: KeyboardEvent): void {
if ((event.target as HTMLElement).tagName !== 'input') return // toggle button can trigger this handler
this.newPassword = (event.target as HTMLInputElement)?.value
this.onPasswordChange()
}
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
this.passwordConfirm = (event.target as HTMLInputElement)?.value
this.onPasswordChange()
}
onPasswordChange(): void {
this.showPasswordConfirm = this.currentPassword !== this.newPassword
if (this.showPasswordConfirm) {
this.form.get('password_confirm').enable()
if (this.newPassword !== this.passwordConfirm) {
if (!this.error) this.error = {}
this.error.password_confirm = $localize`Passwords must match`
} else {
delete this.error?.password_confirm
}
} else {
this.form.get('password_confirm').disable()
delete this.error?.password_confirm
}
}
save(): void {
const passwordChanged = this.currentPassword !== this.newPassword
const profile = Object.assign({}, this.form.value)
this.networkActive = true
this.profileService
.update(profile)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.showInfo($localize`Profile updated successfully`)
if (passwordChanged) {
this.toastService.showInfo(
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
}, 2500)
}
this.activeModal.close()
},
error: (error) => {
this.toastService.showError($localize`Error saving profile`, error)
this.networkActive = false
},
})
}
cancel(): void {
this.activeModal.close()
}
generateAuthToken(): void {
this.profileService.generateAuthToken().subscribe({
next: (token: string) => {
this.form.patchValue({ auth_token: token })
},
error: (error) => {
this.toastService.showError(
$localize`Error generating auth token`,
error
)
},
})
}
copyAuthToken(): void {
this.clipboard.copy(this.form.get('auth_token').value)
this.copied = true
setTimeout(() => {
this.copied = false
}, 3000)
}
}

View File

@@ -37,13 +37,13 @@
</li>
<li class="list-group-item pt-3 pb-2">
<div class="input-group input-group-sm w-100">
<div class="form-check form-switch ms-auto small">
<input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion">
<label class="form-check-label" for="versionSwitch" i18n>Share archive version</label>
<div class="form-check form-switch ms-auto">
<input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [(ngModel)]="archiveVersion">
<label class="form-check-label small" for="versionSwitch" i18n>Share archive version</label>
</div>
</div>
<div class="input-group input-group-sm w-100 mt-2">
<label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label>
<label class="input-group-text" for="addLink">Expires:</label>
<select class="form-select form-select-sm" [(ngModel)]="expirationDays">
<option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option>
</select>

View File

@@ -19,7 +19,6 @@ import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ShareLinksDropdownComponent } from './share-links-dropdown.component'
import { Clipboard } from '@angular/cdk/clipboard'
import { By } from '@angular/platform-browser'
describe('ShareLinksDropdownComponent', () => {
let component: ShareLinksDropdownComponent
@@ -89,7 +88,7 @@ describe('ShareLinksDropdownComponent', () => {
.mockReturnValueOnce(throwError(() => new Error('Unable to get links')))
component.documentId = 99
component.ngOnInit()
component.refresh()
fixture.detectChanges()
expect(toastSpy).toHaveBeenCalled()
})
@@ -98,13 +97,12 @@ describe('ShareLinksDropdownComponent', () => {
const createSpy = jest.spyOn(shareLinkService, 'createLinkForDocument')
component.documentId = 99
component.expirationDays = 7
component.useArchiveVersion = false
component.archiveVersion = false
const expiration = new Date()
expiration.setDate(expiration.getDate() + 7)
const copySpy = jest.spyOn(clipboard, 'copy')
copySpy.mockReturnValue(true)
const refreshSpy = jest.spyOn(component, 'refresh')
component.createLink()
@@ -119,10 +117,8 @@ describe('ShareLinksDropdownComponent', () => {
fixture.detectChanges()
tick(3000)
expect(refreshSpy).toHaveBeenCalled()
expect(copySpy).toHaveBeenCalled()
expect(component.copied).toEqual(1)
tick(100) // copy timeout
expect(refreshSpy).toHaveBeenCalled()
}))
it('should show error on link creation if needed', () => {
@@ -196,36 +192,4 @@ describe('ShareLinksDropdownComponent', () => {
component.share(link)
// expect(navigatorSpy).toHaveBeenCalledWith({ url: component.getShareUrl(link) })
})
it('should correctly generate share URLs', () => {
environment.apiBaseUrl = 'http://example.com/api/'
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
'http://example.com/share/123abc123'
)
environment.apiBaseUrl = 'http://example.domainwithapiinit.com/api/'
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
'http://example.domainwithapiinit.com/share/123abc123'
)
environment.apiBaseUrl = 'http://example.domainwithapiinit.com:1234/api/'
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
'http://example.domainwithapiinit.com:1234/share/123abc123'
)
environment.apiBaseUrl =
'http://example.domainwithapiinit.com:1234/subpath/api/'
expect(component.getShareUrl({ slug: '123abc123' } as any)).toEqual(
'http://example.domainwithapiinit.com:1234/subpath/share/123abc123'
)
})
it('should disable archive switch & option if no archive available', () => {
component.hasArchiveVersion = false
component.ngOnInit()
fixture.detectChanges()
expect(component.useArchiveVersion).toBeFalsy()
expect(
fixture.debugElement.query(By.css("input[type='checkbox']")).attributes[
'ng-reflect-is-disabled'
]
).toBeTruthy()
})
})

View File

@@ -38,9 +38,6 @@ export class ShareLinksDropdownComponent implements OnInit {
@Input()
disabled: boolean = false
@Input()
hasArchiveVersion: boolean = true
shareLinks: PaperlessShareLink[]
loading: boolean = false
@@ -49,7 +46,7 @@ export class ShareLinksDropdownComponent implements OnInit {
expirationDays: number = 7
useArchiveVersion: boolean = true
archiveVersion: boolean = true
constructor(
private shareLinkService: ShareLinkService,
@@ -59,7 +56,6 @@ export class ShareLinksDropdownComponent implements OnInit {
ngOnInit(): void {
if (this._documentId !== undefined) this.refresh()
this.useArchiveVersion = this.hasArchiveVersion
}
refresh() {
@@ -84,10 +80,7 @@ export class ShareLinksDropdownComponent implements OnInit {
}
getShareUrl(link: PaperlessShareLink): string {
const apiURL = new URL(environment.apiBaseUrl)
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
link.slug
}`
return `${environment.apiBaseUrl.replace('api', 'share')}${link.slug}`
}
getDaysRemaining(link: PaperlessShareLink): string {
@@ -98,13 +91,11 @@ export class ShareLinksDropdownComponent implements OnInit {
}
copy(link: PaperlessShareLink) {
const success = this.clipboard.copy(this.getShareUrl(link))
if (success) {
this.copied = link.id
setTimeout(() => {
this.copied = null
}, 3000)
}
this.clipboard.copy(this.getShareUrl(link))
this.copied = link.id
setTimeout(() => {
this.copied = null
}, 3000)
}
canShare(link: PaperlessShareLink): boolean {
@@ -138,7 +129,7 @@ export class ShareLinksDropdownComponent implements OnInit {
this.shareLinkService
.createLinkForDocument(
this._documentId,
this.useArchiveVersion
this.archiveVersion
? PaperlessFileVersion.Archive
: PaperlessFileVersion.Original,
expiration
@@ -146,9 +137,7 @@ export class ShareLinksDropdownComponent implements OnInit {
.subscribe({
next: (result) => {
this.loading = false
setTimeout(() => {
this.copy(result)
}, 10)
this.copy(result)
this.refresh()
},
error: (e) => {

View File

@@ -3,7 +3,7 @@
</pngx-page-header>
<div class="row">
<div class="col-12 col-lg-8 col-xl-9 mb-4">
<div class="col-auto col-lg-8 col-xl-9 mb-4">
<div class="row row-cols-1 g-4" tourAnchor="tour.dashboard"
cdkDropList
[cdkDropListDisabled]="settingsService.globalDropzoneActive"

View File

@@ -63,7 +63,7 @@
</a>
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/storagepaths/">
<a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
<ng-container i18n>Storage Paths</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
</a>

Some files were not shown because too many files have changed in this diff Show More