Merge branch 'dev' into fix-mail-starttls
| @@ -17,3 +17,5 @@ | |||||||
| **/htmlcov | **/htmlcov | ||||||
| /src/.pytest_cache | /src/.pytest_cache | ||||||
| .idea | .idea | ||||||
|  | .venv/ | ||||||
|  | .vscode/ | ||||||
|   | |||||||
| @@ -32,3 +32,6 @@ indent_style = space | |||||||
| # violate it. | # violate it. | ||||||
| [**/test_*.py] | [**/test_*.py] | ||||||
| max_line_length = off | max_line_length = off | ||||||
|  |  | ||||||
|  | [Dockerfile] | ||||||
|  | indent_style = space | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,19 +1,15 @@ | |||||||
|  | <!-- | ||||||
|  | Note: All PRs with code changes should be targeted to the `dev` branch, pure documentation changes can target `main` | ||||||
|  | --> | ||||||
|  |  | ||||||
| ## Proposed change | ## Proposed change | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
| Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your poposed change can be tested. Screenshots and / or videos can also be helpful if appropriate. | Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your proposed change can be tested. Screenshots and / or videos can also be helpful if appropriate. | ||||||
| --> | --> | ||||||
|  |  | ||||||
| Fixes # (issue) | Fixes # (issue) | ||||||
|  |  | ||||||
| <!-- |  | ||||||
| Please also tag the relevant team to help with review. You can tag any of the following: |  | ||||||
| @paperless-ngx/backend (Python / django, database, etc.) |  | ||||||
| @paperless-ngx/frontend (JavaScript/Typescript, HTML, CSS, etc.) |  | ||||||
| @paperless-ngx/ci-cd (GitHub Actions, deployment) |  | ||||||
| @paperless-ngx/test (General testing for larger PRs) |  | ||||||
| --> |  | ||||||
|  |  | ||||||
| ## Type of change | ## Type of change | ||||||
|  |  | ||||||
| <!-- | <!-- | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,7 +8,7 @@ updates: | |||||||
|     target-branch: "dev" |     target-branch: "dev" | ||||||
|     # Look for `package.json` and `lock` files in the `root` directory |     # Look for `package.json` and `lock` files in the `root` directory | ||||||
|     directory: "/src-ui" |     directory: "/src-ui" | ||||||
|     # Check the npm registry for updates every week |     # Check the npm registry for updates every month | ||||||
|     schedule: |     schedule: | ||||||
|       interval: "monthly" |       interval: "monthly" | ||||||
|     # Add reviewers |     # Add reviewers | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | categories: | ||||||
|  |   - title: 'Features' | ||||||
|  |     labels: | ||||||
|  |       - 'enhancement' | ||||||
|  |   - title: 'Bug Fixes' | ||||||
|  |     labels: | ||||||
|  |       - 'bug' | ||||||
|  |   - title: 'Documentation' | ||||||
|  |     label: 'documentation' | ||||||
|  |   - title: 'Maintenance' | ||||||
|  |     labels: | ||||||
|  |       - 'chore' | ||||||
|  |       - 'deployment' | ||||||
|  |       - 'translation' | ||||||
|  |   - title: 'Dependencies' | ||||||
|  |     collapse-after: 3 | ||||||
|  |     label: 'dependencies' | ||||||
|  | include-labels: | ||||||
|  |   - 'enhancement' | ||||||
|  |   - 'bug' | ||||||
|  |   - 'chore' | ||||||
|  |   - 'deployment' | ||||||
|  |   - 'translation' | ||||||
|  |   - 'dependencies' | ||||||
|  | replacers: # Changes "Feature: Update checker" to "Update checker" | ||||||
|  |   - search: '/Feature:|Feat:|\[feature\]/gi' | ||||||
|  |     replace: '' | ||||||
|  | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' | ||||||
|  | change-title-escapes: '\<*_&#@' | ||||||
|  | tag-prefix: "ngx-" | ||||||
|  | template: | | ||||||
|  |   ## Changelog | ||||||
|  |  | ||||||
|  |   $CHANGES | ||||||
							
								
								
									
										104
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -18,13 +18,13 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Checkout |         name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Install pipenv |         name: Install pipenv | ||||||
|         run: pipx install pipenv |         run: pipx install pipenv | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v2 |         uses: actions/setup-python@v3 | ||||||
|         with: |         with: | ||||||
|           python-version: 3.9 |           python-version: 3.9 | ||||||
|           cache: "pipenv" |           cache: "pipenv" | ||||||
| @@ -40,7 +40,7 @@ jobs: | |||||||
|           pipenv run make html |           pipenv run make html | ||||||
|       - |       - | ||||||
|         name: Upload artifact |         name: Upload artifact | ||||||
|         uses: actions/upload-artifact@v2 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: documentation |           name: documentation | ||||||
|           path: docs/_build/html/ |           path: docs/_build/html/ | ||||||
| @@ -51,7 +51,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Checkout |         name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Install checkers |         name: Install checkers | ||||||
|         run: | |         run: | | ||||||
| @@ -78,7 +78,7 @@ jobs: | |||||||
|         uses: psf/black@stable |         uses: psf/black@stable | ||||||
|         with: |         with: | ||||||
|           options: "--check --diff" |           options: "--check --diff" | ||||||
|           version: "22.1.0" |           version: "22.3.0" | ||||||
|       - |       - | ||||||
|         name: Run flake8 checks |         name: Run flake8 checks | ||||||
|         run: | |         run: | | ||||||
| @@ -91,8 +91,8 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Checkout |         name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|       - uses: actions/setup-node@v2 |       - uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: '16' |           node-version: '16' | ||||||
|       - |       - | ||||||
| @@ -115,7 +115,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Checkout |         name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 2 |           fetch-depth: 2 | ||||||
|       - |       - | ||||||
| @@ -123,7 +123,7 @@ jobs: | |||||||
|         run: pipx install pipenv |         run: pipx install pipenv | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v2 |         uses: actions/setup-python@v3 | ||||||
|         with: |         with: | ||||||
|           python-version: "${{ matrix.python-version }}" |           python-version: "${{ matrix.python-version }}" | ||||||
|           cache: "pipenv" |           cache: "pipenv" | ||||||
| @@ -132,7 +132,7 @@ jobs: | |||||||
|         name: Install system dependencies |         name: Install system dependencies | ||||||
|         run: | |         run: | | ||||||
|           sudo apt-get update -qq |           sudo apt-get update -qq | ||||||
|           sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng |           sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng libzbar0 poppler-utils | ||||||
|       - |       - | ||||||
|         name: Install Python dependencies |         name: Install Python dependencies | ||||||
|         run: | |         run: | | ||||||
| @@ -168,14 +168,14 @@ jobs: | |||||||
|   tests-frontend: |   tests-frontend: | ||||||
|     needs: [code-checks-frontend] |     needs: [code-checks-frontend] | ||||||
|     name: "Frontend Tests" |     name: "Frontend Tests" | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-20.04 | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         node-version: [16.x] |         node-version: [16.x] | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v3 | ||||||
|       - name: Use Node.js ${{ matrix.node-version }} |       - name: Use Node.js ${{ matrix.node-version }} | ||||||
|         uses: actions/setup-node@v1 |         uses: actions/setup-node@v3 | ||||||
|         with: |         with: | ||||||
|           node-version: ${{ matrix.node-version }} |           node-version: ${{ matrix.node-version }} | ||||||
|       - run: cd src-ui && npm ci |       - run: cd src-ui && npm ci | ||||||
| @@ -185,31 +185,22 @@ jobs: | |||||||
|   # build and push image to docker hub. |   # build and push image to docker hub. | ||||||
|   build-docker-image: |   build-docker-image: | ||||||
|     if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || startsWith(github.ref, 'refs/tags/ngx-') || startsWith(github.ref, 'refs/tags/beta-')) |     if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || startsWith(github.ref, 'refs/tags/ngx-') || startsWith(github.ref, 'refs/tags/beta-')) | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-20.04 | ||||||
|     needs: [tests-backend, tests-frontend] |     needs: [tests-backend, tests-frontend] | ||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Prepare |         name: Gather Docker metadata | ||||||
|         id: prepare |         id: docker-meta | ||||||
|         run: | |         uses: docker/metadata-action@v3 | ||||||
|           IMAGE_NAME=ghcr.io/${{ github.repository }} |         with: | ||||||
|           if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then |           images: ghcr.io/${{ github.repository }} | ||||||
|             TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/tags/ngx-},${IMAGE_NAME}:latest |           tags: | | ||||||
|             INSPECT_TAG=${IMAGE_NAME}:latest |             type=match,pattern=ngx-(\d.\d.\d),group=1 | ||||||
|           elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then |             type=ref,event=branch | ||||||
|             TAGS=${IMAGE_NAME}:beta |             type=ref,event=tag | ||||||
|             INSPECT_TAG=${TAGS} |  | ||||||
|           elif [[ $GITHUB_REF == refs/heads/* ]]; then |  | ||||||
|             TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/heads/} |  | ||||||
|             INSPECT_TAG=${TAGS} |  | ||||||
|           else |  | ||||||
|             exit 1 |  | ||||||
|           fi |  | ||||||
|           echo ::set-output name=tags::${TAGS} |  | ||||||
|           echo ::set-output name=inspect_tag::${INSPECT_TAG} |  | ||||||
|       - |       - | ||||||
|         name: Checkout |         name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Set up Docker Buildx |         name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v1 | ||||||
| @@ -230,22 +221,23 @@ jobs: | |||||||
|           context: . |           context: . | ||||||
|           file: ./Dockerfile |           file: ./Dockerfile | ||||||
|           platforms: linux/amd64,linux/arm/v7,linux/arm64 |           platforms: linux/amd64,linux/arm/v7,linux/arm64 | ||||||
|           push: true |           push: ${{ github.event_name != 'pull_request' }} | ||||||
|           tags: ${{ steps.prepare.outputs.tags }} |           tags: ${{ steps.docker-meta.outputs.tags }} | ||||||
|  |           labels: ${{ steps.docker-meta.outputs.labels }} | ||||||
|           cache-from: type=gha |           cache-from: type=gha | ||||||
|           cache-to: type=gha,mode=max |           cache-to: type=gha,mode=max | ||||||
|       - |       - | ||||||
|         name: Inspect image |         name: Inspect image | ||||||
|         run: | |         run: | | ||||||
|           docker buildx imagetools inspect ${{ steps.prepare.outputs.inspect_tag }} |           docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} | ||||||
|       - |       - | ||||||
|         name: Export frontend artifact from docker |         name: Export frontend artifact from docker | ||||||
|         run: | |         run: | | ||||||
|           docker run -d --name frontend-extract ${{ steps.prepare.outputs.inspect_tag }} |           docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} | ||||||
|           docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/ |           docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/ | ||||||
|       - |       - | ||||||
|         name: Upload frontend artifact |         name: Upload frontend artifact | ||||||
|         uses: actions/upload-artifact@v2 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: frontend-compiled |           name: frontend-compiled | ||||||
|           path: src/documents/static/frontend/ |           path: src/documents/static/frontend/ | ||||||
| @@ -256,10 +248,10 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Checkout |         name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v2 |         uses: actions/setup-python@v3 | ||||||
|         with: |         with: | ||||||
|           python-version: 3.9 |           python-version: 3.9 | ||||||
|       - |       - | ||||||
| @@ -271,13 +263,13 @@ jobs: | |||||||
|           pip3 install -r requirements.txt |           pip3 install -r requirements.txt | ||||||
|       - |       - | ||||||
|         name: Download frontend artifact |         name: Download frontend artifact | ||||||
|         uses: actions/download-artifact@v2 |         uses: actions/download-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: frontend-compiled |           name: frontend-compiled | ||||||
|           path: src/documents/static/frontend/ |           path: src/documents/static/frontend/ | ||||||
|       - |       - | ||||||
|         name: Download documentation artifact |         name: Download documentation artifact | ||||||
|         uses: actions/download-artifact@v2 |         uses: actions/download-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: documentation |           name: documentation | ||||||
|           path: docs/_build/html/ |           path: docs/_build/html/ | ||||||
| @@ -312,19 +304,19 @@ jobs: | |||||||
|           tar -cJf paperless-ngx.tar.xz paperless-ngx/ |           tar -cJf paperless-ngx.tar.xz paperless-ngx/ | ||||||
|       - |       - | ||||||
|         name: Upload release artifact |         name: Upload release artifact | ||||||
|         uses: actions/upload-artifact@v2 |         uses: actions/upload-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: release |           name: release | ||||||
|           path: dist/paperless-ngx.tar.xz |           path: dist/paperless-ngx.tar.xz | ||||||
|  |  | ||||||
|   publish-release: |   publish-release: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-20.04 | ||||||
|     needs: build-release |     needs: build-release | ||||||
|     if: contains(github.ref, 'refs/tags/ngx-') || contains(github.ref, 'refs/tags/beta-') |     if: contains(github.ref, 'refs/tags/ngx-') || contains(github.ref, 'refs/tags/beta-') | ||||||
|     steps: |     steps: | ||||||
|       - |       - | ||||||
|         name: Download release artifact |         name: Download release artifact | ||||||
|         uses: actions/download-artifact@v2 |         uses: actions/download-artifact@v3 | ||||||
|         with: |         with: | ||||||
|           name: release |           name: release | ||||||
|           path: ./ |           path: ./ | ||||||
| @@ -335,24 +327,22 @@ jobs: | |||||||
|           if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then |           if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then | ||||||
|             echo ::set-output name=version::${GITHUB_REF#refs/tags/ngx-} |             echo ::set-output name=version::${GITHUB_REF#refs/tags/ngx-} | ||||||
|             echo ::set-output name=prerelease::false |             echo ::set-output name=prerelease::false | ||||||
|             echo ::set-output name=body::"For a complete list of changes, see the changelog at https://paperless-ngx.readthedocs.io/en/latest/changelog.html" |  | ||||||
|           elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then |           elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then | ||||||
|             echo ::set-output name=version::${GITHUB_REF#refs/tags/beta-} |             echo ::set-output name=version::${GITHUB_REF#refs/tags/beta-} | ||||||
|             echo ::set-output name=prerelease::true |             echo ::set-output name=prerelease::true | ||||||
|             echo ::set-output name=body::"For a complete list of changes, see the changelog at https://github.com/paperless-ngx/paperless-ngx/blob/beta/docs/changelog.rst" |  | ||||||
|           fi |           fi | ||||||
|       - |       - | ||||||
|         name: Create release |         name: Create Release and Changelog | ||||||
|         id: create_release |         id: create-release | ||||||
|         uses: actions/create-release@v1 |         uses: release-drafter/release-drafter@v5 | ||||||
|  |         with: | ||||||
|  |           name: Paperless-ngx ${{ steps.get_version.outputs.version }} | ||||||
|  |           tag: ngx-${{ steps.get_version.outputs.version }} | ||||||
|  |           version: ${{ steps.get_version.outputs.version }} | ||||||
|  |           prerelease: ${{ steps.get_version.outputs.prerelease }} | ||||||
|  |           publish: true # ensures release is not marked as draft | ||||||
|         env: |         env: | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|         with: |  | ||||||
|           tag_name: ngx-${{ steps.get_version.outputs.version }} |  | ||||||
|           release_name: Paperless-ngx ${{ steps.get_version.outputs.version }} |  | ||||||
|           draft: false |  | ||||||
|           prerelease: ${{ steps.get_version.outputs.prerelease }} |  | ||||||
|           body: ${{ steps.get_version.outputs.body }} |  | ||||||
|       - |       - | ||||||
|         name: Upload release archive |         name: Upload release archive | ||||||
|         id: upload-release-asset |         id: upload-release-asset | ||||||
| @@ -360,7 +350,7 @@ jobs: | |||||||
|         env: |         env: | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|         with: |         with: | ||||||
|           upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps |           upload_url: ${{ steps.create-release.outputs.upload_url }} | ||||||
|           asset_path: ./paperless-ngx.tar.xz |           asset_path: ./paperless-ngx.tar.xz | ||||||
|           asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz |           asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz | ||||||
|           asset_content_type: application/x-xz |           asset_content_type: application/x-xz | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| repos: | repos: | ||||||
|   # General hooks |   # General hooks | ||||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks |   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||||
|     rev: v4.1.0 |     rev: v4.2.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: check-docstring-first |       - id: check-docstring-first | ||||||
|       - id: check-json |       - id: check-json | ||||||
| @@ -27,7 +27,7 @@ repos: | |||||||
|       - id: check-case-conflict |       - id: check-case-conflict | ||||||
|       - id: detect-private-key |       - id: detect-private-key | ||||||
|   - repo: https://github.com/pre-commit/mirrors-prettier |   - repo: https://github.com/pre-commit/mirrors-prettier | ||||||
|     rev: "v2.5.1" |     rev: "v2.6.2" | ||||||
|     hooks: |     hooks: | ||||||
|       - id: prettier |       - id: prettier | ||||||
|         types_or: |         types_or: | ||||||
| @@ -37,7 +37,7 @@ repos: | |||||||
|         exclude: "(^Pipfile\\.lock$)" |         exclude: "(^Pipfile\\.lock$)" | ||||||
|   # Python hooks |   # Python hooks | ||||||
|   - repo: https://github.com/asottile/reorder_python_imports |   - repo: https://github.com/asottile/reorder_python_imports | ||||||
|     rev: v2.7.1 |     rev: v3.0.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: reorder-python-imports |       - id: reorder-python-imports | ||||||
|         exclude: "(migrations)" |         exclude: "(migrations)" | ||||||
| @@ -47,7 +47,7 @@ repos: | |||||||
|       - id: yesqa |       - id: yesqa | ||||||
|         exclude: "(migrations)" |         exclude: "(migrations)" | ||||||
|   - repo: https://github.com/asottile/add-trailing-comma |   - repo: https://github.com/asottile/add-trailing-comma | ||||||
|     rev: "v2.2.1" |     rev: "v2.2.2" | ||||||
|     hooks: |     hooks: | ||||||
|       - id: add-trailing-comma |       - id: add-trailing-comma | ||||||
|         exclude: "(migrations)" |         exclude: "(migrations)" | ||||||
| @@ -59,7 +59,7 @@ repos: | |||||||
|         args: |         args: | ||||||
|           - "--config=./src/setup.cfg" |           - "--config=./src/setup.cfg" | ||||||
|   - repo: https://github.com/psf/black |   - repo: https://github.com/psf/black | ||||||
|     rev: 22.1.0 |     rev: 22.3.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: black |       - id: black | ||||||
|   # Dockerfile hooks |   # Dockerfile hooks | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								CODEOWNERS
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | |||||||
|  | /.github/workflows/ @paperless-ngx/ci-cd | ||||||
|  | /docker/ @paperless-ngx/ci-cd | ||||||
|  | /scripts/ @paperless-ngx/ci-cd | ||||||
|  |  | ||||||
|  | /src-ui/ @paperless-ngx/frontend | ||||||
|  |  | ||||||
|  | /src/ @paperless-ngx/backend | ||||||
|  | Pipfile* @paperless-ngx/backend | ||||||
|  | *.py @paperless-ngx/backend | ||||||
|  | requirements.txt @paperless-ngx/backend | ||||||
							
								
								
									
										146
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -6,69 +6,13 @@ WORKDIR /src/src-ui | |||||||
| RUN npm update npm -g && npm ci --no-optional | RUN npm update npm -g && npm ci --no-optional | ||||||
| RUN ./node_modules/.bin/ng build --configuration production | RUN ./node_modules/.bin/ng build --configuration production | ||||||
|  |  | ||||||
|  | FROM ghcr.io/paperless-ngx/builder/ngx-base:dev as main-app | ||||||
|  |  | ||||||
| FROM ubuntu:20.04 AS jbig2enc | LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>" | ||||||
|  | LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/" | ||||||
| WORKDIR /usr/src/jbig2enc | LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx" | ||||||
|  | LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx" | ||||||
| RUN apt-get update \ | LABEL org.opencontainers.image.licenses="GPL-3.0-only" | ||||||
|   && apt-get install -y --no-install-recommends build-essential \ |  | ||||||
|     automake \ |  | ||||||
|     libtool \ |  | ||||||
|     libleptonica-dev \ |  | ||||||
|     zlib1g-dev \ |  | ||||||
|     git \ |  | ||||||
|     ca-certificates \ |  | ||||||
|   && rm -rf /var/lib/apt/lists/* |  | ||||||
|  |  | ||||||
| RUN git clone https://github.com/agl/jbig2enc . |  | ||||||
| RUN ./autogen.sh |  | ||||||
| RUN ./configure && make |  | ||||||
|  |  | ||||||
|  |  | ||||||
| FROM python:3.9-slim-bullseye |  | ||||||
|  |  | ||||||
| # Binary dependencies |  | ||||||
| RUN apt-get update \ |  | ||||||
| 	&& apt-get -y --no-install-recommends install \ |  | ||||||
|   	# Basic dependencies |  | ||||||
| 		curl \ |  | ||||||
| 		gnupg \ |  | ||||||
| 		imagemagick \ |  | ||||||
| 		gettext \ |  | ||||||
| 		tzdata \ |  | ||||||
| 		gosu \ |  | ||||||
| 		# fonts for text file thumbnail generation |  | ||||||
| 		fonts-liberation \ |  | ||||||
| 		# for Numpy |  | ||||||
| 		libatlas-base-dev \ |  | ||||||
| 		libxslt1-dev \ |  | ||||||
| 		# thumbnail size reduction |  | ||||||
| 		optipng \ |  | ||||||
| 		libxml2 \ |  | ||||||
| 		pngquant \ |  | ||||||
| 		unpaper \ |  | ||||||
| 		zlib1g \ |  | ||||||
| 		ghostscript \ |  | ||||||
| 		icc-profiles-free \ |  | ||||||
|   	# Mime type detection |  | ||||||
| 		file \ |  | ||||||
| 		libmagic-dev \ |  | ||||||
| 		media-types \ |  | ||||||
| 		# OCRmyPDF dependencies |  | ||||||
| 		liblept5 \ |  | ||||||
| 		tesseract-ocr \ |  | ||||||
| 		tesseract-ocr-eng \ |  | ||||||
| 		tesseract-ocr-deu \ |  | ||||||
| 		tesseract-ocr-fra \ |  | ||||||
| 		tesseract-ocr-ita \ |  | ||||||
| 		tesseract-ocr-spa \ |  | ||||||
|   && rm -rf /var/lib/apt/lists/* |  | ||||||
|  |  | ||||||
| # copy jbig2enc |  | ||||||
| COPY --from=jbig2enc /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/ |  | ||||||
| COPY --from=jbig2enc /usr/src/jbig2enc/src/jbig2 /usr/local/bin/ |  | ||||||
| COPY --from=jbig2enc /usr/src/jbig2enc/src/*.h /usr/local/include/ |  | ||||||
|  |  | ||||||
| WORKDIR /usr/src/paperless/src/ | WORKDIR /usr/src/paperless/src/ | ||||||
|  |  | ||||||
| @@ -76,47 +20,31 @@ COPY requirements.txt ../ | |||||||
|  |  | ||||||
| # Python dependencies | # Python dependencies | ||||||
| RUN apt-get update \ | RUN apt-get update \ | ||||||
|  |   # python-Levenshtein still needs to be compiled here | ||||||
|   && apt-get -y --no-install-recommends install \ |   && apt-get -y --no-install-recommends install \ | ||||||
| 		build-essential \ |     build-essential \ | ||||||
| 		libpq-dev \ |     && python3 -m pip install --upgrade --no-cache-dir pip wheel \ | ||||||
| 		git \ |   && python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \ | ||||||
| 		zlib1g-dev \ |   && python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \ | ||||||
| 		libjpeg62-turbo-dev \ |   && apt-get -y purge build-essential \ | ||||||
| 	&& if [ "$(uname -m)" = "armv7l" ] || [ "$(uname -m)" = "aarch64" ]; \ |   && apt-get -y autoremove --purge \ | ||||||
| 	  then echo "Building qpdf" \ |   && rm -rf /var/lib/apt/lists/* | ||||||
| 	  && mkdir -p /usr/src/qpdf \ |  | ||||||
| 	  && cd /usr/src/qpdf \ |  | ||||||
| 	  && git clone https://github.com/qpdf/qpdf.git . \ |  | ||||||
| 	  && git checkout --quiet release-qpdf-10.6.2 \ |  | ||||||
| 	  && ./configure \ |  | ||||||
| 	  && make \ |  | ||||||
| 	  && make install \ |  | ||||||
| 	  && cd /usr/src/paperless/src/ \ |  | ||||||
| 	  && rm -rf /usr/src/qpdf; \ |  | ||||||
| 	else \ |  | ||||||
| 	  echo "Skipping qpdf build because pikepdf binary wheels are available."; \ |  | ||||||
| 	fi \ |  | ||||||
|     && python3 -m pip install --upgrade pip wheel \ |  | ||||||
| 	&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \ |  | ||||||
|   	&& python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \ |  | ||||||
| 	&& apt-get -y purge build-essential git zlib1g-dev libjpeg62-turbo-dev \ |  | ||||||
| 	&& apt-get -y autoremove --purge \ |  | ||||||
| 	&& rm -rf /var/lib/apt/lists/* |  | ||||||
|  |  | ||||||
| # setup docker-specific things | # setup docker-specific things | ||||||
| COPY docker/ ./docker/ | COPY docker/ ./docker/ | ||||||
|  |  | ||||||
| RUN cd docker \ | RUN cd docker \ | ||||||
|   	&& cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ |     && cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ | ||||||
| 	&& mkdir /var/log/supervisord /var/run/supervisord \ |   && mkdir /var/log/supervisord /var/run/supervisord \ | ||||||
| 	&& cp supervisord.conf /etc/supervisord.conf \ |   && cp supervisord.conf /etc/supervisord.conf \ | ||||||
| 	&& cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \ |   && cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \ | ||||||
| 	&& cp docker-prepare.sh /sbin/docker-prepare.sh \ |   && chmod 755 /sbin/docker-entrypoint.sh \ | ||||||
| 	&& chmod 755 /sbin/docker-entrypoint.sh \ |   && cp docker-prepare.sh /sbin/docker-prepare.sh \ | ||||||
| 	&& chmod +x install_management_commands.sh \ |   && chmod 755 /sbin/docker-prepare.sh \ | ||||||
| 	&& ./install_management_commands.sh \ |   && chmod +x install_management_commands.sh \ | ||||||
| 	&& cd .. \ |   && ./install_management_commands.sh \ | ||||||
| 	&& rm docker -rf |   && cd .. \ | ||||||
|  |   && rm -rf docker/ | ||||||
|  |  | ||||||
| COPY gunicorn.conf.py ../ | COPY gunicorn.conf.py ../ | ||||||
|  |  | ||||||
| @@ -125,18 +53,18 @@ COPY --from=compile-frontend /src/src/ ./ | |||||||
|  |  | ||||||
| # add users, setup scripts | # add users, setup scripts | ||||||
| RUN addgroup --gid 1000 paperless \ | RUN addgroup --gid 1000 paperless \ | ||||||
| 	&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \ |   && useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \ | ||||||
| 	&& chown -R paperless:paperless ../ \ |   && chown -R paperless:paperless ../ \ | ||||||
| 	&& gosu paperless python3 manage.py collectstatic --clear --no-input \ |   && gosu paperless python3 manage.py collectstatic --clear --no-input \ | ||||||
| 	&& gosu paperless python3 manage.py compilemessages |   && gosu paperless python3 manage.py compilemessages | ||||||
|  |  | ||||||
|  | VOLUME ["/usr/src/paperless/data", \ | ||||||
|  |         "/usr/src/paperless/media", \ | ||||||
|  |         "/usr/src/paperless/consume", \ | ||||||
|  |         "/usr/src/paperless/export"] | ||||||
|  |  | ||||||
| VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"] |  | ||||||
| ENTRYPOINT ["/sbin/docker-entrypoint.sh"] | ENTRYPOINT ["/sbin/docker-entrypoint.sh"] | ||||||
| EXPOSE 8000 |  | ||||||
| CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"] |  | ||||||
|  |  | ||||||
| LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>" | EXPOSE 8000 | ||||||
| LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/" |  | ||||||
| LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx" | CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"] | ||||||
| LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx" |  | ||||||
| LABEL org.opencontainers.image.licenses="GPL-3.0-only" |  | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						| @@ -22,13 +22,15 @@ gunicorn = "*" | |||||||
| imap-tools = "~=0.53.0" | imap-tools = "~=0.53.0" | ||||||
| langdetect = "*" | langdetect = "*" | ||||||
| pathvalidate = "*" | pathvalidate = "*" | ||||||
| pillow = "~=9.0" | pillow = "~=9.1" | ||||||
|  | # Any version update to pikepdf requires a base image update | ||||||
| pikepdf = "~=5.1" | pikepdf = "~=5.1" | ||||||
| python-gnupg = "*" | python-gnupg = "*" | ||||||
| python-dotenv = "*" | python-dotenv = "*" | ||||||
| python-dateutil = "*" | python-dateutil = "*" | ||||||
| python-magic = "*" | python-magic = "*" | ||||||
| psycopg2-binary = "*" | # Any version update to psycopg2 requires a base image update | ||||||
|  | psycopg2 = "*" | ||||||
| redis = "*" | redis = "*" | ||||||
| # Pinned because aarch64 wheels and updates cause warnings when loading the classifier model. | # Pinned because aarch64 wheels and updates cause warnings when loading the classifier model. | ||||||
| scikit-learn="==1.0.2" | scikit-learn="==1.0.2" | ||||||
| @@ -49,6 +51,8 @@ concurrent-log-handler = "*" | |||||||
| "backports.zoneinfo" = {version = "*", markers = "python_version < '3.9'"} | "backports.zoneinfo" = {version = "*", markers = "python_version < '3.9'"} | ||||||
| "importlib-resources" = {version = "*", markers = "python_version < '3.9'"} | "importlib-resources" = {version = "*", markers = "python_version < '3.9'"} | ||||||
| zipp = {version = "*", markers = "python_version < '3.9'"} | zipp = {version = "*", markers = "python_version < '3.9'"} | ||||||
|  | pyzbar = "*" | ||||||
|  | pdf2image = "*" | ||||||
|  |  | ||||||
| [dev-packages] | [dev-packages] | ||||||
| coveralls = "*" | coveralls = "*" | ||||||
| @@ -60,7 +64,7 @@ pytest-django = "*" | |||||||
| pytest-env = "*" | pytest-env = "*" | ||||||
| pytest-sugar = "*" | pytest-sugar = "*" | ||||||
| pytest-xdist = "*" | pytest-xdist = "*" | ||||||
| sphinx = "~=4.4.0" | sphinx = "~=4.5.0" | ||||||
| sphinx_rtd_theme = "*" | sphinx_rtd_theme = "*" | ||||||
| tox = "*" | tox = "*" | ||||||
| black = "*" | black = "*" | ||||||
|   | |||||||
							
								
								
									
										562
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "7e76d6b807f96506f56c1bddb36b44deda6745014e5ed7c94f047fc1eb972eb8" |             "sha256": "9573af313c811561d467d814c52c6bd1439bc48e3b31d7f56afed5f0ebe4b648" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": {}, |         "requires": {}, | ||||||
| @@ -68,10 +68,10 @@ | |||||||
|         }, |         }, | ||||||
|         "autobahn": { |         "autobahn": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:60e1f4c602aacd052ffe3d46ae40b6b75f8286b3c46922c213b523162e58c17e" |                 "sha256:58a887c7a196bb08d8b6624cb3695f493a9e5c9f00fd350d8d6f829b47ff9036" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==22.2.2" |             "version": "==22.3.2" | ||||||
|         }, |         }, | ||||||
|         "automat": { |         "automat": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -99,7 +99,6 @@ | |||||||
|                 "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", |                 "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", | ||||||
|                 "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" |                 "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |  | ||||||
|             "markers": "python_version < '3.9'", |             "markers": "python_version < '3.9'", | ||||||
|             "version": "==0.2.1" |             "version": "==0.2.1" | ||||||
|         }, |         }, | ||||||
| @@ -207,11 +206,11 @@ | |||||||
|         }, |         }, | ||||||
|         "click": { |         "click": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", |                 "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", | ||||||
|                 "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" |                 "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==8.0.4" |             "version": "==8.1.2" | ||||||
|         }, |         }, | ||||||
|         "coloredlogs": { |         "coloredlogs": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -280,11 +279,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django": { |         "django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1239218849e922033a35d2a2f777cb8bee18bd725416744074f455f34ff50d0c", |                 "sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687", | ||||||
|                 "sha256:77ff2e7050e3324c9b67e29b6707754566f58514112a9ac73310f60cd5261930" |                 "sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==4.0.3" |             "version": "==4.0.4" | ||||||
|         }, |         }, | ||||||
|         "django-cors-headers": { |         "django-cors-headers": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -488,18 +487,17 @@ | |||||||
|         }, |         }, | ||||||
|         "img2pdf": { |         "img2pdf": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:8e51c5043efa95d751481b516071a006f87c2a4059961a9ac43ec238915de09f" |                 "sha256:8ec898a9646523fd3862b154f3f47cd52609c24cc3e2dc1fb5f0168f0cbe793c" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.4.3" |             "version": "==0.4.4" | ||||||
|         }, |         }, | ||||||
|         "importlib-resources": { |         "importlib-resources": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45", |                 "sha256:1b93238cbf23b4cde34240dd8321d99e9bf2eb4bc91c0c99b2886283e7baad85", | ||||||
|                 "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b" |                 "sha256:a9dd72f6cc106aeb50f6e66b86b69b454766dd6e39b69ac68450253058706bcc" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |  | ||||||
|             "markers": "python_version < '3.9'", |             "markers": "python_version < '3.9'", | ||||||
|             "version": "==5.4.0" |             "version": "==5.6.0" | ||||||
|         }, |         }, | ||||||
|         "incremental": { |         "incremental": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -673,11 +671,11 @@ | |||||||
|         }, |         }, | ||||||
|         "ocrmypdf": { |         "ocrmypdf": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:201ed2f589f851be73908fce35fbb6fb05e4739289d3cd8765f9519f49ea1cd9", |                 "sha256:7f0a6165b80ba1b37ce5943cf5b4faf93bf98c04c8f5157ef83c5f292491485f", | ||||||
|                 "sha256:f42e60bc2b6534634dd08928584275b1c556dc875c849650afcc38f7da9e2856" |                 "sha256:d52410bc38cf5b66da27668e38c66ac41fd3136457c1ec388b311f0a78ee213c" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==13.4.1" |             "version": "==13.4.2" | ||||||
|         }, |         }, | ||||||
|         "packaging": { |         "packaging": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -695,87 +693,98 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.5.0" |             "version": "==2.5.0" | ||||||
|         }, |         }, | ||||||
|         "pdfminer.six": { |         "pdf2image": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0351f17d362ee2d48b158be52bcde6576d96460efd038a3e89a043fba6d634d7", |                 "sha256:84f79f2b8fad943e36323ea4e937fcb05f26ded0caa0a01181df66049e42fb65", | ||||||
|                 "sha256:d3efb75c0249b51c1bf795e3a8bddf1726b276c77bf75fb136adea471ee2825b" |                 "sha256:d58ed94d978a70c73c2bb7fdf8acbaf2a7089c29ff8141be5f45433c0c4293bb" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==20211012" |             "version": "==1.16.0" | ||||||
|  |         }, | ||||||
|  |         "pdfminer.six": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:af0630f98a292bad4170f54e80f82ca81b916dd0b2c996437ec45c02f11d8762", | ||||||
|  |                 "sha256:eff2ce0abeaa4df94dc3461f70eab104487c7b4a2b3c7e9fd0aeec6c5f44d6a6" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==20220319" | ||||||
|         }, |         }, | ||||||
|         "pikepdf": { |         "pikepdf": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1567b74d15c16d1bef56a6d5f56fb6a35f4cc022ae252d35f33f56bb16b87966", |                 "sha256:01be838a44430c4be84b748a33950fed09892472934a8041596c11189f365f7f", | ||||||
|                 "sha256:179f24b4a4d0e89c7f592d85ba4daf7d34c709eb0a691425b881413fdce70734", |                 "sha256:0cc95ef470169dfa5acc9196299bdba236716234a0d8b2746e2a563bc6f1f456", | ||||||
|                 "sha256:22201b06db627a86cc91c1b76491dd9c57fce2df4e3bc8ba700ff66f7f7da04a", |                 "sha256:13e72d0aeeb3fc452569a3f7994acdd007de9aad804ced734d57cec269261b8b", | ||||||
|                 "sha256:3e1a0b9ecf5d4aa106c3c0db558952f9a15f343f812c3bba6d6e1a56e25224ed", |                 "sha256:2873503522ef26a09a6020c29c2efd221fa2ddc31e83bd902be27d317144cf63", | ||||||
|                 "sha256:45ce2479e5ba74896ef389abf92831d7fbb34f25e6557adb9115710223a0bb13", |                 "sha256:2d5d6d3248b33ca5961d84bc3121a299cd27237fad56868d815e381c9a98d3d1", | ||||||
|                 "sha256:4fa5c8494b011b19bd198dd9c3cd94676b905360b2231ad35171ae586644b823", |                 "sha256:2f62e6c7bcf5d631e6ea74cf861f3e816f587c6ccb4ecbf6ac862e088ba2e4ac", | ||||||
|                 "sha256:559b3d502cc1a6813cbcb0766b0797fec034303f8f9b0734cf938fb1734e2b74", |                 "sha256:51694d3d2f90510da6a8d7a4d07313ca868b373fffec6de270d9bbff1ce37180", | ||||||
|                 "sha256:61731fceaab99850bc7045232301c2332bba727f78b53f7038fcbdcaf3d64309", |                 "sha256:5c23cbd7ae71f08fb5b5d9660eb0bc61abf345ada01bea6e1b6884c4261e17d6", | ||||||
|                 "sha256:6460d489341e7f8dc3f6b0dbf1f5a75a918ebd1e0ecb4c2b00877264a68ee1f4", |                 "sha256:6371bf02a436be2b7c63322b83a8e47523f2cd16438b2e93d546c7caf9ae308d", | ||||||
|                 "sha256:716ca6fc8947502cb73a517c884066afa132ca998e085a309b8fb8c5796d6277", |                 "sha256:657293b74af8c7cf03f9905218a7935b26a4f3006803016b40b3db78e04cb35c", | ||||||
|                 "sha256:726450eb9baaad5697687c2621d481c80f868b68c06d2cff4be3f6a7ce28cfee", |                 "sha256:680d47377bb9fd6a36b6a81464ee269b4b29cbf29a84ae4f2ab8f6ea3665bf69", | ||||||
|                 "sha256:7e9c247ca384ad1606281eda4d841bc8cbff90875979ac3b520bcc5404bf9b26", |                 "sha256:710535c679ab0d7b8249f72247832773e7a9a121dfbe9cad7f6465bd9bb45fae", | ||||||
|                 "sha256:8333d813b452daa4a066e135fd7ab6f7c07ccc02cb8381455d61d74f0a0ad0fe", |                 "sha256:7b4d7c09036d863915cb01007ca183d6fe64e2d57c0472453097bc9e029a58fb", | ||||||
|                 "sha256:83d0af374b103934de033f096205143fa9d6f78e789ba78c8aa6dfb0e5b73bc3", |                 "sha256:978b6388ae99a024bdcae5a322c68e90c187cb568d09d43e6586b3479267121d", | ||||||
|                 "sha256:878c1c95298486d8cea7e8236c70613e7eae1426cbf362c3883ecd06e8f9c2d9", |                 "sha256:9917a03d500aab72715a9236136af7a5c8c7b26c034bf71ebdf028e177f0d25f", | ||||||
|                 "sha256:9abef24d929c4a08292dc4be4d6c4e5bf93832e747eef5f39e854348a332f46a", |                 "sha256:996faa6b119488f96d7271672a22af86e56e5544ec6b8eae6cd7d4432c70ae2d", | ||||||
|                 "sha256:a474241dbeda246356b6448f607f4fb9fee5b9f5cb19511a768b88b471325865", |                 "sha256:9bac9e9d6b28dc0cc5a554051f183fbd070d0f9fe63c4e9aca939b8c44a5bb4d", | ||||||
|                 "sha256:a553fd06e5f6e78c5e840d066e7c8b1a988e16489fe0bd4a143ca601809ea4dd", |                 "sha256:aac14061de06843759ea6f5777fd8d7b71af808ed9264f57483a3311a09788ab", | ||||||
|                 "sha256:a6154c6bb7606ef534444f54271a410a6337cee54dcdef20f0fa0686f622cf50", |                 "sha256:ad5361c3669fc0c8dbaf8fa0a590bddf59fad256bb2c527d5ce5cf991743a240", | ||||||
|                 "sha256:ac0082379cf6aa6c0c682bee4a3d2adbe6d60b9125e3632876d7c5e9665c07ab", |                 "sha256:bc40b30c37f8f7c5bef873eca1f04e91ce34b6b74507d8d0019238a17d281fdc", | ||||||
|                 "sha256:c22e3fbfb76ad7838dde82c8d9fafd4c09fd419cee531b31d1b48a07344ac2b4", |                 "sha256:bd9faae19787a5d05b9fcbe84d7cfe4d44e318068e06eca18906b9dba45425b6", | ||||||
|                 "sha256:d42c52ff2e8fb00fad14182f67a8f076f38c75a874123b6776aa5c6af09e1126", |                 "sha256:c64e7905ec438b7a6c12626f2859df87f471892fab75b65b1441d9e1b38b4dde", | ||||||
|                 "sha256:d6ef14b722f80351e15c9163e0aa1f1df84f065c080765f8232ec51af6bd3368", |                 "sha256:d4db409b21a8ec0d3a79d2bbd894b997b13223c9ccf341cdc31b64360f1ee4c7", | ||||||
|                 "sha256:d71a38445c80972572248815ebe61f9c814c53925a6b83b6596f3482a98a5f25", |                 "sha256:e0b635d6d9faefb4d0d32722279b8eb4e4d5d7b596c426f3433343de65e0c772", | ||||||
|                 "sha256:e81ebbdd53257f411827bbb301900cdbdca34ca60b4f7248f80c6e6980062498", |                 "sha256:e62e9e8afe77fe2f06715faf10f38a4810d282d66f1e9e05208bb8d9723e6acf", | ||||||
|                 "sha256:f52f4ed655c25c408e8454cbcf0e7223b62f635d1e8ab738fd0c2d46531d28ed", |                 "sha256:f85d309bcfeeb3e2d344346a5050bfc41e332f19d390f79c20e4fc7de4b10a17", | ||||||
|                 "sha256:fb407ae5820ee0bf71022d5a8f539d709dac590443270a13baf6a8872c76d46f" |                 "sha256:fe3fc2efe498aba6204b85c17c6a5d54ab7303354ecc5c3da624a6b6af0b3406" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==5.1.0" |             "version": "==5.1.1" | ||||||
|         }, |         }, | ||||||
|         "pillow": { |         "pillow": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97", |                 "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", | ||||||
|                 "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049", |                 "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", | ||||||
|                 "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c", |                 "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", | ||||||
|                 "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae", |                 "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", | ||||||
|                 "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28", |                 "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", | ||||||
|                 "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030", |                 "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", | ||||||
|                 "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56", |                 "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", | ||||||
|                 "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976", |                 "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", | ||||||
|                 "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e", |                 "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", | ||||||
|                 "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e", |                 "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", | ||||||
|                 "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f", |                 "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", | ||||||
|                 "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b", |                 "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", | ||||||
|                 "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a", |                 "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", | ||||||
|                 "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e", |                 "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", | ||||||
|                 "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa", |                 "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", | ||||||
|                 "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7", |                 "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", | ||||||
|                 "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00", |                 "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", | ||||||
|                 "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838", |                 "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", | ||||||
|                 "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360", |                 "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", | ||||||
|                 "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b", |                 "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", | ||||||
|                 "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a", |                 "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", | ||||||
|                 "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd", |                 "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", | ||||||
|                 "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4", |                 "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", | ||||||
|                 "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70", |                 "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", | ||||||
|                 "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204", |                 "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", | ||||||
|                 "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc", |                 "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", | ||||||
|                 "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b", |                 "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", | ||||||
|                 "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669", |                 "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", | ||||||
|                 "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7", |                 "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", | ||||||
|                 "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e", |                 "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", | ||||||
|                 "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c", |                 "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", | ||||||
|                 "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092", |                 "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", | ||||||
|                 "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c", |                 "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", | ||||||
|                 "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5", |                 "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", | ||||||
|                 "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac" |                 "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", | ||||||
|  |                 "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", | ||||||
|  |                 "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", | ||||||
|  |                 "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==9.0.1" |             "version": "==9.1.0" | ||||||
|         }, |         }, | ||||||
|         "pluggy": { |         "pluggy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -793,64 +802,19 @@ | |||||||
|             "markers": "python_version >= '3'", |             "markers": "python_version >= '3'", | ||||||
|             "version": "==2.4.0" |             "version": "==2.4.0" | ||||||
|         }, |         }, | ||||||
|         "psycopg2-binary": { |         "psycopg2": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7", |                 "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c", | ||||||
|                 "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76", |                 "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf", | ||||||
|                 "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa", |                 "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362", | ||||||
|                 "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9", |                 "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7", | ||||||
|                 "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004", |                 "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461", | ||||||
|                 "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1", |                 "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126", | ||||||
|                 "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094", |                 "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981", | ||||||
|                 "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57", |                 "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56", | ||||||
|                 "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af", |                 "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305", | ||||||
|                 "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554", |                 "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2", | ||||||
|                 "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232", |                 "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca" | ||||||
|                 "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c", |  | ||||||
|                 "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b", |  | ||||||
|                 "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834", |  | ||||||
|                 "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2", |  | ||||||
|                 "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71", |  | ||||||
|                 "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460", |  | ||||||
|                 "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e", |  | ||||||
|                 "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4", |  | ||||||
|                 "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d", |  | ||||||
|                 "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d", |  | ||||||
|                 "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9", |  | ||||||
|                 "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f", |  | ||||||
|                 "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063", |  | ||||||
|                 "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478", |  | ||||||
|                 "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092", |  | ||||||
|                 "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c", |  | ||||||
|                 "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce", |  | ||||||
|                 "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1", |  | ||||||
|                 "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65", |  | ||||||
|                 "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e", |  | ||||||
|                 "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4", |  | ||||||
|                 "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029", |  | ||||||
|                 "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33", |  | ||||||
|                 "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39", |  | ||||||
|                 "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53", |  | ||||||
|                 "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307", |  | ||||||
|                 "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42", |  | ||||||
|                 "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35", |  | ||||||
|                 "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8", |  | ||||||
|                 "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb", |  | ||||||
|                 "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae", |  | ||||||
|                 "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e", |  | ||||||
|                 "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f", |  | ||||||
|                 "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba", |  | ||||||
|                 "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24", |  | ||||||
|                 "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca", |  | ||||||
|                 "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb", |  | ||||||
|                 "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef", |  | ||||||
|                 "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42", |  | ||||||
|                 "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1", |  | ||||||
|                 "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667", |  | ||||||
|                 "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272", |  | ||||||
|                 "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281", |  | ||||||
|                 "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e", |  | ||||||
|                 "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd" |  | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.9.3" |             "version": "==2.9.3" | ||||||
| @@ -907,11 +871,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pyparsing": { |         "pyparsing": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", |                 "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", | ||||||
|                 "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" |                 "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_full_version >= '3.6.8'", | ||||||
|             "version": "==3.0.7" |             "version": "==3.0.8" | ||||||
|         }, |         }, | ||||||
|         "python-dateutil": { |         "python-dateutil": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -923,11 +887,11 @@ | |||||||
|         }, |         }, | ||||||
|         "python-dotenv": { |         "python-dotenv": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3", |                 "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", | ||||||
|                 "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f" |                 "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.19.2" |             "version": "==0.20.0" | ||||||
|         }, |         }, | ||||||
|         "python-gnupg": { |         "python-gnupg": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1004,6 +968,15 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==6.0" |             "version": "==6.0" | ||||||
|         }, |         }, | ||||||
|  |         "pyzbar": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c", | ||||||
|  |                 "sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d", | ||||||
|  |                 "sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==0.1.9" | ||||||
|  |         }, | ||||||
|         "redis": { |         "redis": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", |                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", | ||||||
| @@ -1094,45 +1067,45 @@ | |||||||
|         }, |         }, | ||||||
|         "reportlab": { |         "reportlab": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:02a67c8caaf669c63f0c410525137b162d2ade43b43e10bf1ac19b1919f6aa95", |                 "sha256:09b2ca175129a34292399fc4c6a8b1739f6c5946368fcaa6f931d69385b2f720", | ||||||
|                 "sha256:03f2612f2213b78e31a2f1d580881eca89267874ae61e432a33f64df50f590a7", |                 "sha256:0a7f2b7232c3ffb451b649d55c51a6dd0c8104ad7bbcfe355addf7619705e7fa", | ||||||
|                 "sha256:05627acc324ce213c79fbbcc012d0a576bef8bc540fe0a875a6491c78612b6f4", |                 "sha256:0e767cf4507ca8eed7dde8511f0889b0f19f160a2bdf9ef07742b2aaeceed9f2", | ||||||
|                 "sha256:068000debb926a4dec6442de6e1fff831f5a6cecfe830d715926c82d8f4eeb0b", |                 "sha256:10681d89a0ca37bb4036283fb8c0efac9ac1b22265dbdf350bda0448be33e00c", | ||||||
|                 "sha256:088359b44b418be27b8fb38dd733ac6c3dd611fad10e6dbba3876027ef216e58", |                 "sha256:15294435f786968bcdf1a7a67bcc23a136470b6ea26919497f5c76ff0f653041", | ||||||
|                 "sha256:0aabf425f215dc297052194166f951e940c077271d0133bfdd3a08bb56022d6c", |                 "sha256:193671445b4885128d8800d3e416eb2fa4fd89bafae08cc9889c0752fe5ad8c2", | ||||||
|                 "sha256:12d9582d9a6cd18bf3f61b355c13261baeb22bc0e994f385750eae9d89ce6846", |                 "sha256:1967dbc9930917d75c39784712a137d432dbc2e5ca9e132a2453319c2619ccff", | ||||||
|                 "sha256:1aad24ddfdfcd89f2db7cd10298de55a6e3a6dfc482b2aabf98880986c550fcc", |                 "sha256:1ec84055cf2c83783958b74eadf0e577eb0cd9088c8b5d536e9ddc0f4a9f8c70", | ||||||
|                 "sha256:1be429316812a4c3aacfbee0ef9a84def4a3a0cba37d6c9155563ff8a8a04c8b", |                 "sha256:23f5aed2d212096f2fe95d56f868d63f839a08bf7e389237e644d93981274222", | ||||||
|                 "sha256:1c32ed1c42bbce03faa02e33d1949cc2ca5eda42c52267cacce28f69cb087663", |                 "sha256:32a5c5cd9625a40feec956f460355b4813bc3187c4f8dc9efd9f1a7f8f854e34", | ||||||
|                 "sha256:1c51729484e0e1784812746c84c8c97215b95b02ba75057bb5dbe4f206a93c64", |                 "sha256:37dda88dbe16dd3f4f9039464637cce66e462c0b95e5763dbd45ac5799136d3a", | ||||||
|                 "sha256:1cc100be35dba31ee6865de26d460ade8a4aef451b90c0ec2cc6b7cc5f293440", |                 "sha256:496f42840604255ce06777bc129048b3bab966213bbac4f07fbe4ceb6a2e0482", | ||||||
|                 "sha256:244b17fc544d0cd41f61648b247fa2fa8c4e1d47602f5fd6ff4ddd7b29f35642", |                 "sha256:4ba8eebfa4383e4680d6e7e6dba9c45c1fe19bbc0a754db4d84823f1a9511e56", | ||||||
|                 "sha256:25ca368637467d617cd73fd5e68b31f3ceed2db42a175b76951e32bc97345ed7", |                 "sha256:4fbe23ac870adf90544d2014c572dba6ec4d772afad6505bb91f171ddad12839", | ||||||
|                 "sha256:31ebc2997c1e57df0cc6a55a83c63e629c8420482bf994e0665e289c4b603c63", |                 "sha256:50f8e30f5410efc69b0217261b1f21912888da392a4549e79c7aaaac85f01bfa", | ||||||
|                 "sha256:3842f9e815924f9880e4a127afd9b22607f701b352513880b9dd6bcf3a651bb4", |                 "sha256:5d0cc3682456ad213150f6dbffe7d47eab737d809e517c316103376be548fb84", | ||||||
|                 "sha256:53ec9fbd6e0c5ac9ee3f349700af8f1bb886878c802086b4d9d0b981def239a9", |                 "sha256:6a114761ad3ba6e0cdfacf14a8fb2cb8f5713b115ca1f0c17f3cd638d0a5b4bd", | ||||||
|                 "sha256:5da9b84b645e7e7b8f4a219e4d3b5bfce771d0e11f34f861bde4ef5c4b4fc4e6", |                 "sha256:713574da534b6ce73d884f1574c35a565e438af4888fcc75e752f1de02e356a7", | ||||||
|                 "sha256:603e9980f99cdf1a3325a49c092ad0847e3fe032aa59d9f69421f81b4e2199de", |                 "sha256:8cb82b6d14ad4bd915acacc8f114c6a7bab8b9b1503cabb930e433ebd320f90c", | ||||||
|                 "sha256:60bf28718e50f6d28b3e784c64b29bf73477773537ed7b4177aa90e4a54c2323", |                 "sha256:90f74627cafecf3924741ab8b0690a19df4214eb56b1cfce2dc74a15c9744034", | ||||||
|                 "sha256:6415f16e64c0179ecb2f6231e3433014c5c837ce589661aa8b954944764a8d31", |                 "sha256:92a6613af9877e3ad2a1c5a16a122514a4f9f8d9b91b1f22e7fa0fa796617b36", | ||||||
|                 "sha256:6566fa308633661ec9053ce4dcd145fb10b6f9cc0cf7aa2fee84e9e8f2c77d2d", |                 "sha256:a441afdfe31870b964bccde042d7172ed3c0077f519bbf3ed7d9d34c406b6b91", | ||||||
|                 "sha256:662ee549793e9b38ecb5dae2521352ff73f05c2816665327835a3a12abe36edc", |                 "sha256:ab1ffe4ec7be99ad348791116d436610afdc7a9a02a968997f31eaa62eaadad8", | ||||||
|                 "sha256:71617eac54a207ae24e6ac78feee02aca61d5954844ada786c186748c9517565", |                 "sha256:b2c2fd861f10b2cd49ccf29a31da9ad5c3b95aa437804e4fd0351ed4eb695f74", | ||||||
|                 "sha256:76257019f254b655b95e88df7ebd1c39ac90543987e968f6e5bdf91797d012f3", |                 "sha256:bbaab798991863952c593c0459dcb82e0aade837675593310e13cba2ce7fb45a", | ||||||
|                 "sha256:7c730dc5421b1b39cbaa39098ef9c7a79f216ab479718eb27bbd0fc3947ddea0", |                 "sha256:c9a5f63bc381c0f945402ef4c1bccc74a8eed28f6be6596704b1db7d82ec89fe", | ||||||
|                 "sha256:9aa2a746bfbd7878af74d22ed3c2cfc7b920dedf47d943146a20a6e196ab4fcd", |                 "sha256:cb21666fc9edec9716553bfcfe0c30d1bbbe2731910a96f07ec65652974e5f83", | ||||||
|                 "sha256:9b2b28fe14de1124c310cc349dcb71bb3018ffa11d9eeb4ed7e8acd2570fb8b0", |                 "sha256:ce3a3aad287c8532f62223f5720b5504e31abe3dce52a27bd2a25f508c0d846e", | ||||||
|                 "sha256:a12dd3ddc2950adbe47d874fd0b675f67d724600eb96da8ab72dc37f9d4d71b2", |                 "sha256:cebd0b28a0e875a9ce789514700f80659269ecf2a8fcef0aa10b8ae52b40474a", | ||||||
|                 "sha256:a8d071c30166deb03c4f99af1d8f48355f8acbe4d05b7b5c3a616a41b2bae3ad", |                 "sha256:d1bf9455aff37beb421a4447d89d6dd77bb46f677c0bab4eb0272cdb79faad2f", | ||||||
|                 "sha256:a9218f499ac42133090f16e33a622eff69248753b5c6738c0ee7b916fa084752", |                 "sha256:d927bf802bf53c1b5a3878a22e9be310900877984e7c436a3a99bdd19cfec4c3", | ||||||
|                 "sha256:b41ed7754de6d1702065c53e5e6f266571eca4139f875bc127849d9c8238a704", |                 "sha256:de724c78f4eb1363b1195dce85a2a8806e7509b69ac5c842a714d942ea534d63", | ||||||
|                 "sha256:b4efd9b1b9bcc95e41e80130be00e89b1ea56b816a362d5f2eb2f141df624ad9", |                 "sha256:e1fc1b1f5d9d1c2e18b5e60602dfa7854b2330ba0efc312ef605abf588abea9c", | ||||||
|                 "sha256:cd0f0cd614a6fdc3b5a76351e9462956fd4d9b62b0e3ca5e7767259768905818", |                 "sha256:e492e87886423192af1fafde23907bcd9d2fdccfc22f67e18aa5c73db3a380a3", | ||||||
|                 "sha256:dc7657fcb0bc3e485c3c869a44dddb52d711356a01a456664b7bef827222c982", |                 "sha256:e9b5e9115363545a727d8ebe7e4b94f7cf6f26113261a269d50d88b8db4eb726", | ||||||
|                 "sha256:fdfc56c20b77f0a8ddcdfd13e4dec916dc9c8f1dc59c27611fa7d69e2cfb9a4a" |                 "sha256:ff0e014a3a3fe286c642ef51213c41684a156b9ed293ef205e8890bc1dbbfdc7" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6' and python_version < '4'", |             "markers": "python_version >= '3.7' and python_version < '4'", | ||||||
|             "version": "==3.6.8" |             "version": "==3.6.9" | ||||||
|         }, |         }, | ||||||
|         "requests": { |         "requests": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1218,11 +1191,11 @@ | |||||||
|         }, |         }, | ||||||
|         "setuptools": { |         "setuptools": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c", |                 "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", | ||||||
|                 "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96" |                 "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==60.10.0" |             "version": "==62.1.0" | ||||||
|         }, |         }, | ||||||
|         "six": { |         "six": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1265,22 +1238,22 @@ | |||||||
|         }, |         }, | ||||||
|         "tqdm": { |         "tqdm": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1d9835ede8e394bb8c9dcbffbca02d717217113adc679236873eeaac5bc0b3cd", |                 "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d", | ||||||
|                 "sha256:e643e071046f17139dea55b880dc9b33822ce21613b4a4f5ea57f202833dbc29" |                 "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==4.63.0" |             "version": "==4.64.0" | ||||||
|         }, |         }, | ||||||
|         "twisted": { |         "twisted": { | ||||||
|             "extras": [ |             "extras": [ | ||||||
|                 "tls" |                 "tls" | ||||||
|             ], |             ], | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:57f32b1f6838facb8c004c89467840367ad38e9e535f8252091345dba500b4f2", |                 "sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680", | ||||||
|                 "sha256:5c63c149eb6b8fe1e32a0215b1cef96fabdba04f705d8efb9174b1ccf5b49d49" |                 "sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_full_version >= '3.6.7'", |             "markers": "python_full_version >= '3.6.7'", | ||||||
|             "version": "==22.2.0" |             "version": "==22.4.0" | ||||||
|         }, |         }, | ||||||
|         "txaio": { |         "txaio": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1308,11 +1281,11 @@ | |||||||
|         }, |         }, | ||||||
|         "tzlocal": { |         "tzlocal": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09", |                 "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745", | ||||||
|                 "sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f" |                 "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==4.1" |             "version": "==4.2" | ||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1356,39 +1329,40 @@ | |||||||
|         }, |         }, | ||||||
|         "watchdog": { |         "watchdog": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685", |                 "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385", | ||||||
|                 "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04", |                 "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690", | ||||||
|                 "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb", |                 "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a", | ||||||
|                 "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542", |                 "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383", | ||||||
|                 "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6", |                 "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99", | ||||||
|                 "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b", |                 "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4", | ||||||
|                 "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660", |                 "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd", | ||||||
|                 "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3", |                 "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566", | ||||||
|                 "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923", |                 "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572", | ||||||
|                 "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7", |                 "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480", | ||||||
|                 "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b", |                 "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6", | ||||||
|                 "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669", |                 "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa", | ||||||
|                 "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2", |                 "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8", | ||||||
|                 "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3", |                 "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca", | ||||||
|                 "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604", |                 "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab", | ||||||
|                 "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8", |                 "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd", | ||||||
|                 "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5", |                 "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055", | ||||||
|                 "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0", |                 "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601", | ||||||
|                 "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6", |                 "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c", | ||||||
|                 "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65", |                 "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b", | ||||||
|                 "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d", |                 "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2", | ||||||
|                 "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15", |                 "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f", | ||||||
|                 "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9" |                 "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420", | ||||||
|  |                 "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.1.6" |             "version": "==2.1.7" | ||||||
|         }, |         }, | ||||||
|         "watchgod": { |         "watchgod": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:4ba20c2fa3e63df706ab50e694b9453b05395fadb7cbbfd984d71fb1547d485d", |                 "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce", | ||||||
|                 "sha256:c12d15f3df7d11e740704e45398277f75f1d78f46ad59ca9d7505bfd8b8d3086" |                 "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.8.1" |             "version": "==0.8.2" | ||||||
|         }, |         }, | ||||||
|         "wcwidth": { |         "wcwidth": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1469,12 +1443,11 @@ | |||||||
|         }, |         }, | ||||||
|         "zipp": { |         "zipp": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", |                 "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", | ||||||
|                 "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" |                 "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |  | ||||||
|             "markers": "python_version < '3.9'", |             "markers": "python_version < '3.9'", | ||||||
|             "version": "==3.7.0" |             "version": "==3.8.0" | ||||||
|         }, |         }, | ||||||
|         "zope.interface": { |         "zope.interface": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1560,32 +1533,32 @@ | |||||||
|         }, |         }, | ||||||
|         "black": { |         "black": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2", |                 "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", | ||||||
|                 "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71", |                 "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", | ||||||
|                 "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6", |                 "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", | ||||||
|                 "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5", |                 "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", | ||||||
|                 "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912", |                 "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", | ||||||
|                 "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866", |                 "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", | ||||||
|                 "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d", |                 "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", | ||||||
|                 "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0", |                 "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", | ||||||
|                 "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321", |                 "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", | ||||||
|                 "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8", |                 "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", | ||||||
|                 "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd", |                 "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", | ||||||
|                 "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3", |                 "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", | ||||||
|                 "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba", |                 "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", | ||||||
|                 "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0", |                 "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", | ||||||
|                 "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5", |                 "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", | ||||||
|                 "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a", |                 "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", | ||||||
|                 "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28", |                 "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", | ||||||
|                 "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c", |                 "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", | ||||||
|                 "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1", |                 "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", | ||||||
|                 "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab", |                 "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", | ||||||
|                 "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f", |                 "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", | ||||||
|                 "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61", |                 "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", | ||||||
|                 "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3" |                 "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==22.1.0" |             "version": "==22.3.0" | ||||||
|         }, |         }, | ||||||
|         "certifi": { |         "certifi": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1612,15 +1585,15 @@ | |||||||
|         }, |         }, | ||||||
|         "click": { |         "click": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", |                 "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", | ||||||
|                 "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" |                 "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==8.0.4" |             "version": "==8.1.2" | ||||||
|         }, |         }, | ||||||
|         "coverage": { |         "coverage": { | ||||||
|             "extras": [ |             "extras": [ | ||||||
|                 "toml" |  | ||||||
|             ], |             ], | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", |                 "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", | ||||||
| @@ -1715,11 +1688,11 @@ | |||||||
|         }, |         }, | ||||||
|         "faker": { |         "faker": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:66db859b6abe376d02e805ad81eb8dcfce38f0945f17ee7cdf74ed349985ea52", |                 "sha256:188961065fb5c78ea639f42176f55100f72c90c3a3179ac6c955c4bd712b0511", | ||||||
|                 "sha256:fe969607836ce7100e38b88dcb598aacb733d895e6e9401894dd603e35623000" |                 "sha256:7758ece2593ce603db117db3d27393c31f4af03f783e176f3f0e14839a4f3426" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==13.3.2" |             "version": "==13.3.4" | ||||||
|         }, |         }, | ||||||
|         "filelock": { |         "filelock": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1753,14 +1726,6 @@ | |||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.3.0" |             "version": "==1.3.0" | ||||||
|         }, |         }, | ||||||
|         "importlib-metadata": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", |  | ||||||
|                 "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" |  | ||||||
|             ], |  | ||||||
|             "markers": "python_version < '3.10'", |  | ||||||
|             "version": "==4.11.3" |  | ||||||
|         }, |  | ||||||
|         "iniconfig": { |         "iniconfig": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", |                 "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", | ||||||
| @@ -1770,11 +1735,11 @@ | |||||||
|         }, |         }, | ||||||
|         "jinja2": { |         "jinja2": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", |                 "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119", | ||||||
|                 "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" |                 "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==3.0.3" |             "version": "==3.1.1" | ||||||
|         }, |         }, | ||||||
|         "markupsafe": { |         "markupsafe": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1869,11 +1834,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pre-commit": { |         "pre-commit": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616", |                 "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2", | ||||||
|                 "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a" |                 "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.17.0" |             "version": "==2.18.1" | ||||||
|         }, |         }, | ||||||
|         "py": { |         "py": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -1901,11 +1866,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pyparsing": { |         "pyparsing": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", |                 "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", | ||||||
|                 "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" |                 "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_full_version >= '3.6.8'", | ||||||
|             "version": "==3.0.7" |             "version": "==3.0.8" | ||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -2039,11 +2004,11 @@ | |||||||
|         }, |         }, | ||||||
|         "sphinx": { |         "sphinx": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe", |                 "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6", | ||||||
|                 "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc" |                 "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==4.4.0" |             "version": "==4.5.0" | ||||||
|         }, |         }, | ||||||
|         "sphinx-rtd-theme": { |         "sphinx-rtd-theme": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -2131,14 +2096,6 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.24.5" |             "version": "==3.24.5" | ||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", |  | ||||||
|                 "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" |  | ||||||
|             ], |  | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==4.1.1" |  | ||||||
|         }, |  | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", |                 "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", | ||||||
| @@ -2149,20 +2106,11 @@ | |||||||
|         }, |         }, | ||||||
|         "virtualenv": { |         "virtualenv": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:c3e01300fb8495bc00ed70741f5271fc95fed067eb7106297be73d30879af60c", |                 "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a", | ||||||
|                 "sha256:ce8901d3bbf3b90393498187f2d56797a8a452fb2d0d7efc6fd837554d6f679c" |                 "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==20.13.4" |             "version": "==20.14.1" | ||||||
|         }, |  | ||||||
|         "zipp": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", |  | ||||||
|                 "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" |  | ||||||
|             ], |  | ||||||
|             "index": "pypi", |  | ||||||
|             "markers": "python_version < '3.9'", |  | ||||||
|             "version": "==3.7.0" |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,6 +22,10 @@ | |||||||
| # Docker setup does not use the configuration file. | # Docker setup does not use the configuration file. | ||||||
| # A few commonly adjusted settings are provided below. | # A few commonly adjusted settings are provided below. | ||||||
|  |  | ||||||
|  | # This is required if you will be exposing Paperless-ngx on a public domain | ||||||
|  | # (if doing so please consider security measures such as reverse proxy) | ||||||
|  | #PAPERLESS_URL=https://paperless.example.com | ||||||
|  |  | ||||||
| # Adjust this key if you plan to make paperless available publicly. It should | # Adjust this key if you plan to make paperless available publicly. It should | ||||||
| # be a very long sequence of random characters. You don't need to remember it. | # be a very long sequence of random characters. You don't need to remember it. | ||||||
| #PAPERLESS_SECRET_KEY=change-me | #PAPERLESS_SECRET_KEY=change-me | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| #!/bin/bash | #!/usr/bin/env bash | ||||||
|  |  | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| #!/bin/bash | #!/usr/bin/env bash | ||||||
|  |  | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
|   | |||||||
| @@ -200,7 +200,7 @@ Troubleshooting: | |||||||
| - Check your script's permission e.g. in case of permission error ``sudo chmod 755 post-consumption-example.sh`` | - 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. ``echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`` | - Pipe your scripts's output to a log file e.g. ``echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`` | ||||||
|  |  | ||||||
| .. _post-consumption-example.sh: https://github.com/jonaswinkler/paperless-ngx/blob/master/scripts/post-consumption-example.sh | .. _post-consumption-example.sh: https://github.com/paperless-ngx/paperless-ngx/blob/main/scripts/post-consumption-example.sh | ||||||
|  |  | ||||||
| .. _advanced-file_name_handling: | .. _advanced-file_name_handling: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ The endpoints correctly serve the response header fields ``Content-Disposition`` | |||||||
| and ``Content-Type`` to indicate the filename for download and the type of content of | and ``Content-Type`` to indicate the filename for download and the type of content of | ||||||
| the document. | the document. | ||||||
|  |  | ||||||
| In order to download or preview the original document when an archied document is available, | In order to download or preview the original document when an archived document is available, | ||||||
| supply the query parameter ``original=true``. | supply the query parameter ``original=true``. | ||||||
|  |  | ||||||
| .. hint:: | .. hint:: | ||||||
|   | |||||||
| @@ -142,7 +142,24 @@ PAPERLESS_SECRET_KEY=<key> | |||||||
|  |  | ||||||
|     Default is listed in the file ``src/paperless/settings.py``. |     Default is listed in the file ``src/paperless/settings.py``. | ||||||
|  |  | ||||||
| PAPERLESS_ALLOWED_HOSTS<comma-separated-list> | PAPERLESS_URL=<url> | ||||||
|  |     This setting can be used to set the three options below (ALLOWED_HOSTS, | ||||||
|  |     CORS_ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS). If the other options are | ||||||
|  |     set the values will be combined with this one. Do not include a trailing | ||||||
|  |     slash. E.g. https://paperless.domain.com | ||||||
|  |  | ||||||
|  |     Defaults to empty string, leaving the other settings unaffected. | ||||||
|  |  | ||||||
|  | PAPERLESS_CSRF_TRUSTED_ORIGINS=<comma-separated-list> | ||||||
|  |     A list of trusted origins for unsafe requests (e.g. POST). As of Django 4.0 | ||||||
|  |     this is required to access the Django admin via the web. | ||||||
|  |     See https://docs.djangoproject.com/en/4.0/ref/settings/#csrf-trusted-origins | ||||||
|  |  | ||||||
|  |     Can also be set using PAPERLESS_URL (see above). | ||||||
|  |  | ||||||
|  |     Defaults to empty string, which does not add any origins to the trusted list. | ||||||
|  |  | ||||||
|  | PAPERLESS_ALLOWED_HOSTS=<comma-separated-list> | ||||||
|     If you're planning on putting Paperless on the open internet, then you |     If you're planning on putting Paperless on the open internet, then you | ||||||
|     really should set this value to the domain name you're using.  Failing to do |     really should set this value to the domain name you're using.  Failing to do | ||||||
|     so leaves you open to HTTP host header attacks: |     so leaves you open to HTTP host header attacks: | ||||||
| @@ -151,12 +168,16 @@ PAPERLESS_ALLOWED_HOSTS<comma-separated-list> | |||||||
|     Just remember that this is a comma-separated list, so "example.com" is fine, |     Just remember that this is a comma-separated list, so "example.com" is fine, | ||||||
|     as is "example.com,www.example.com", but NOT " example.com" or "example.com," |     as is "example.com,www.example.com", but NOT " example.com" or "example.com," | ||||||
|  |  | ||||||
|  |     Can also be set using PAPERLESS_URL (see above). | ||||||
|  |  | ||||||
|     Defaults to "*", which is all hosts. |     Defaults to "*", which is all hosts. | ||||||
|  |  | ||||||
| PAPERLESS_CORS_ALLOWED_HOSTS<comma-separated-list> | PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list> | ||||||
|     You need to add your servers to the list of allowed hosts that can do CORS |     You need to add your servers to the list of allowed hosts that can do CORS | ||||||
|     calls. Set this to your public domain name. |     calls. Set this to your public domain name. | ||||||
|  |  | ||||||
|  |     Can also be set using PAPERLESS_URL (see above). | ||||||
|  |  | ||||||
|     Defaults to "http://localhost:8000". |     Defaults to "http://localhost:8000". | ||||||
|  |  | ||||||
| PAPERLESS_FORCE_SCRIPT_NAME=<path> | PAPERLESS_FORCE_SCRIPT_NAME=<path> | ||||||
| @@ -389,6 +410,15 @@ PAPERLESS_OCR_IMAGE_DPI=<num> | |||||||
|     Default is none, which will automatically calculate image DPI so that |     Default is none, which will automatically calculate image DPI so that | ||||||
|     the produced PDF documents are A4 sized. |     the produced PDF documents are A4 sized. | ||||||
|  |  | ||||||
|  | PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num> | ||||||
|  |     Paperless will not OCR images that have more pixels than this limit. | ||||||
|  |     This is intended to prevent decompression bombs from overloading paperless. | ||||||
|  |     Increasing this limit is desired if you face a DecompressionBombError despite | ||||||
|  |     the concerning file not being malicious; this could e.g. be caused by invalidly | ||||||
|  |     recognized metadata. | ||||||
|  |     If you have enough resources or if you are certain that your uploaded files | ||||||
|  |     are not malicious you can increase this value to your needs. | ||||||
|  |     The default value is 256000000, an image with more pixels than that would not be parsed. | ||||||
|  |  | ||||||
| PAPERLESS_OCR_USER_ARGS=<json> | PAPERLESS_OCR_USER_ARGS=<json> | ||||||
|     OCRmyPDF offers many more options. Use this parameter to specify any |     OCRmyPDF offers many more options. Use this parameter to specify any | ||||||
| @@ -531,6 +561,10 @@ PAPERLESS_WORKER_TIMEOUT=<num> | |||||||
|     large documents within the default 1800 seconds. So extending this timeout |     large documents within the default 1800 seconds. So extending this timeout | ||||||
|     may prove to be useful on weak hardware setups. |     may prove to be useful on weak hardware setups. | ||||||
|  |  | ||||||
|  | PAPERLESS_WORKER_RETRY=<num> | ||||||
|  |     If PAPERLESS_WORKER_TIMEOUT has been configured, the retry time for a task can | ||||||
|  |     also be configured.  By default, this value will be set to 10s more than the | ||||||
|  |     worker timeout.  This value should never be set less than the worker timeout. | ||||||
|  |  | ||||||
| PAPERLESS_TIME_ZONE=<timezone> | PAPERLESS_TIME_ZONE=<timezone> | ||||||
|     Set the time zone here. |     Set the time zone here. | ||||||
| @@ -579,6 +613,27 @@ PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=<bool> | |||||||
|  |  | ||||||
|     Defaults to false. |     Defaults to false. | ||||||
|  |  | ||||||
|  | PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool> | ||||||
|  |     Enables the scanning and page separation based on detected barcodes. | ||||||
|  |     This allows for scanning and adding multiple documents per uploaded | ||||||
|  |     file, which are separated by one or multiple barcode pages. | ||||||
|  |  | ||||||
|  |     For ease of use, it is suggested to use a standardized separation page, | ||||||
|  |     e.g. `here <https://www.alliancegroup.co.uk/patch-codes.htm>`_. | ||||||
|  |  | ||||||
|  |     If no barcodes are detected in the uploaded file, no page separation | ||||||
|  |     will happen. | ||||||
|  |  | ||||||
|  |     Defaults to false. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT | ||||||
|  |   Defines the string to be detected as a separator barcode. | ||||||
|  |   If paperless is used with the PATCH-T separator pages, users | ||||||
|  |   shouldn't change this. | ||||||
|  |  | ||||||
|  |   Defaults to "PATCHT" | ||||||
|  |  | ||||||
|  |  | ||||||
| PAPERLESS_CONVERT_MEMORY_LIMIT=<num> | PAPERLESS_CONVERT_MEMORY_LIMIT=<num> | ||||||
|     On smaller systems, or even in the case of Very Large Documents, the consumer |     On smaller systems, or even in the case of Very Large Documents, the consumer | ||||||
| @@ -662,7 +717,7 @@ PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json> | |||||||
|  |  | ||||||
|     This can be adjusted by configuring a custom json array with patterns to exclude. |     This can be adjusted by configuring a custom json array with patterns to exclude. | ||||||
|  |  | ||||||
|     Defautls to ``[".DS_STORE/*", "._*", ".stfolder/*"]``. |     Defaults to ``[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]``. | ||||||
|  |  | ||||||
| Binaries | Binaries | ||||||
| ######## | ######## | ||||||
| @@ -755,3 +810,26 @@ PAPERLESS_OCR_LANGUAGES=<list> | |||||||
|         PAPERLESS_OCR_LANGUAGE=tur |         PAPERLESS_OCR_LANGUAGE=tur | ||||||
|  |  | ||||||
|     Defaults to none, which does not install any additional languages. |     Defaults to none, which does not install any additional languages. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .. _configuration-update-checking: | ||||||
|  |  | ||||||
|  | Update Checking | ||||||
|  | ############### | ||||||
|  |  | ||||||
|  | PAPERLESS_ENABLE_UPDATE_CHECK=<bool> | ||||||
|  |     Enable (or disable) the automatic check for available updates. This feature is disabled | ||||||
|  |     by default but if it is not explicitly set Paperless-ngx will show a message about this. | ||||||
|  |  | ||||||
|  |     If enabled, the feature works by pinging the the Github API for the latest release e.g. | ||||||
|  |     https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest | ||||||
|  |     to determine whether a new version is available. | ||||||
|  |  | ||||||
|  |     Actual updating of the app must still be performed manually. | ||||||
|  |  | ||||||
|  |     Note that for users of thirdy-party containers e.g. linuxserver.io this notification | ||||||
|  |     may be 'ahead' of a new release from the third-party maintainers. | ||||||
|  |  | ||||||
|  |     In either case, no tracking data is collected by the app in any way. | ||||||
|  |  | ||||||
|  |     Defaults to none, which disables the feature. | ||||||
|   | |||||||
| @@ -34,6 +34,8 @@ it fixed for everyone! | |||||||
| Before contributing please review our `code of conduct`_ and other important | Before contributing please review our `code of conduct`_ and other important | ||||||
| information in the `contributing guidelines`_. | information in the `contributing guidelines`_. | ||||||
|  |  | ||||||
|  | .. _code-formatting-with-pre-commit-hooks: | ||||||
|  |  | ||||||
| Code formatting with pre-commit Hooks | Code formatting with pre-commit Hooks | ||||||
| ===================================== | ===================================== | ||||||
|  |  | ||||||
| @@ -85,6 +87,7 @@ To do the setup you need to perform the steps from the following chapters in a c | |||||||
|             docker run -d -p 6379:6379 --restart unless-stopped redis:latest |             docker run -d -p 6379:6379 --restart unless-stopped redis:latest | ||||||
|  |  | ||||||
| 7.  Install the python dependencies by performing in the src/ directory. | 7.  Install the python dependencies by performing in the src/ directory. | ||||||
|  |  | ||||||
|     .. code:: shell-session |     .. code:: shell-session | ||||||
|  |  | ||||||
|         pipenv install --dev |         pipenv install --dev | ||||||
| @@ -183,6 +186,31 @@ X-Frame-Options are in place so that the front end behaves exactly as in product | |||||||
| relies on you being logged into the back end. Without a valid session, The front end will simply | relies on you being logged into the back end. Without a valid session, The front end will simply | ||||||
| not work. | not work. | ||||||
|  |  | ||||||
|  | Testing and code style: | ||||||
|  |  | ||||||
|  | *   The frontend code (.ts, .html, .scss) use ``prettier`` for code formatting via the Git | ||||||
|  |     ``pre-commit`` hooks which run automatically on commit. See | ||||||
|  |     :ref:`above <code-formatting-with-pre-commit-hooks>` for installation. You can also run this | ||||||
|  |     via cli with a command such as | ||||||
|  |  | ||||||
|  |     .. code:: shell-session | ||||||
|  |  | ||||||
|  |         $ git ls-files -- '*.ts' | xargs pre-commit run prettier --files | ||||||
|  |  | ||||||
|  | *   Frontend testing uses jest and cypress. There is currently a need for significantly more | ||||||
|  |     frontend tests. Unit tests and e2e tests, respectively, can be run non-interactively with: | ||||||
|  |  | ||||||
|  |     .. code:: shell-session | ||||||
|  |  | ||||||
|  |         $ ng test | ||||||
|  |         $ npm run e2e:ci | ||||||
|  |  | ||||||
|  |     Cypress also includes a UI which can be run from within the ``src-ui`` directory with | ||||||
|  |  | ||||||
|  |     .. code:: shell-session | ||||||
|  |  | ||||||
|  |         $ ./node_modules/.bin/cypress open | ||||||
|  |  | ||||||
| In order to build the front end and serve it as part of django, execute | In order to build the front end and serve it as part of django, execute | ||||||
|  |  | ||||||
| .. code:: shell-session | .. code:: shell-session | ||||||
|   | |||||||
| @@ -13,43 +13,43 @@ that works right for you based on recommendations from other Paperless users. | |||||||
| Physical scanners | Physical scanners | ||||||
| ================= | ================= | ||||||
|  |  | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brand   | Model          | Supports                          | Recommended By | | | Brand   | Model          | Supports                                 | Recommended By | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| |         |                | FTP | NFS | SMB | SMTP | API [1]_ |                | | |         |                | FTP | SFTP | NFS | SMB | SMTP | API [1]_ |                | | ||||||
| +=========+================+=====+=====+=====+======+==========+================+ | +=========+================+=====+======+=====+=====+======+==========+================+ | ||||||
| | Brother | `ADS-1700W`_   | yes |     | yes | yes  |          |`holzhannes`_   | | | Brother | `ADS-1700W`_   | yes |      |     | yes | yes  |          |`holzhannes`_   | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `ADS-1600W`_   | yes |     | yes | yes  |          |`holzhannes`_   | | | Brother | `ADS-1600W`_   | yes |      |     | yes | yes  |          |`holzhannes`_   | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `ADS-1500W`_   | yes |     | yes | yes  |          |`danielquinn`_  | | | Brother | `ADS-1500W`_   | yes |      |     | yes | yes  |          |`danielquinn`_  | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `ADS-1100W`_   | yes |     |     |      |          |`ytzelf`_       | | | Brother | `ADS-1100W`_   | yes |      |     |     |      |          |`ytzelf`_       | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `ADS-2800W`_   | yes | yes |     | yes  | yes      |`philpagel`_    | | | Brother | `ADS-2800W`_   | yes | yes  |     | yes | yes  |          |`philpagel`_    | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `MFC-J6930DW`_ | yes |     |     |      |          |`ayounggun`_    | | | Brother | `MFC-J6930DW`_ | yes |      |     |     |      |          |`ayounggun`_    | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `MFC-L5850DW`_ | yes |     |     | yes  |          |`holzhannes`_   | | | Brother | `MFC-L5850DW`_ | yes |      |     |     | yes  |          |`holzhannes`_   | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `MFC-L2750DW`_ | yes |     | yes | yes  |          |`muued`_        | | | Brother | `MFC-L2750DW`_ | yes |      |     | yes | yes  |          |`muued`_        | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `MFC-J5910DW`_ | yes |     |     |      |          |`bmsleight`_    | | | Brother | `MFC-J5910DW`_ | yes |      |     |     |      |          |`bmsleight`_    | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `MFC-8950DW`_  | yes |     |     | yes  | yes      |`philpagel`_    | | | Brother | `MFC-8950DW`_  | yes |      |     | yes | yes  |          |`philpagel`_    | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Brother | `MFC-9142CDN`_ | yes |     | yes |      |          |`REOLDEV`_      | | | Brother | `MFC-9142CDN`_ | yes |      |     | yes |      |          |`REOLDEV`_      | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Fujitsu | `ix500`_       | yes |     | yes |      |          |`eonist`_       | | | Fujitsu | `ix500`_       | yes |      |     | yes |      |          |`eonist`_       | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Epson   | `ES-580W`_     | yes |     | yes | yes  |          |`fignew`_       | | | Epson   | `ES-580W`_     | yes |      |     | yes | yes  |          |`fignew`_       | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Epson   | `WF-7710DWF`_  | yes |     | yes |      |          |`Skylinar`_     | | | Epson   | `WF-7710DWF`_  | yes |      |     | yes |      |          |`Skylinar`_     | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Fujitsu | `S1300i`_      | yes |     | yes |      |          |`jonaswinkler`_ | | | Fujitsu | `S1300i`_      | yes |      |     | yes |      |          |`jonaswinkler`_ | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
| | Doxie   | `Q2`_          |     |     |     |      | yes      |`Unkn0wnCat`_   | | | Doxie   | `Q2`_          |     |      |     |     |      | yes      |`Unkn0wnCat`_   | | ||||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||||
|  |  | ||||||
| .. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw | .. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw | ||||||
| .. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw | .. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw | ||||||
| @@ -131,4 +131,3 @@ This part assumes your Doxie is connected to WiFi and you know its IP. | |||||||
| 6. Click *Submit* at the bottom of the page | 6. Click *Submit* at the bottom of the page | ||||||
|  |  | ||||||
| Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance! | Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance! | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,6 +25,19 @@ Check for the following issues: | |||||||
| *   Go to the admin interface, and check if there are failed tasks. If so, the | *   Go to the admin interface, and check if there are failed tasks. If so, the | ||||||
|     tasks will contain an error message. |     tasks will contain an error message. | ||||||
|  |  | ||||||
|  | Consumer warns ``OCR for XX failed`` | ||||||
|  | #################################### | ||||||
|  |  | ||||||
|  | If you find the OCR accuracy to be too low, and/or the document consumer warns | ||||||
|  | that ``OCR for XX failed, but we're going to stick with what we've got since | ||||||
|  | FORGIVING_OCR is enabled``, then you might need to install the | ||||||
|  | `Tesseract language files <http://packages.ubuntu.com/search?keywords=tesseract-ocr>`_ | ||||||
|  | marching your document's languages. | ||||||
|  |  | ||||||
|  | As an example, if you are running Paperless-ngx from any Ubuntu or Debian | ||||||
|  | box, and your documents are written in Spanish you may need to run:: | ||||||
|  |  | ||||||
|  |     apt-get install -y tesseract-ocr-spa | ||||||
|  |  | ||||||
| Consumer fails to pickup any new files | Consumer fails to pickup any new files | ||||||
| ###################################### | ###################################### | ||||||
|   | |||||||
| @@ -178,6 +178,14 @@ These are as follows: | |||||||
|     automatically or manually and tell paperless to move them to yet another folder |     automatically or manually and tell paperless to move them to yet another folder | ||||||
|     after consumption. It's up to you. |     after consumption. It's up to you. | ||||||
|  |  | ||||||
|  | .. note:: | ||||||
|  |  | ||||||
|  |     When defining a mail rule with a folder, you may need to try different characters to | ||||||
|  |     define how the sub-folders are separated.  Common values include ".", "/" or "|", but | ||||||
|  |     this varies by the mail server.  Unfortunately, this isn't a value we can determine | ||||||
|  |     automatically.  Either check the documentation for your mail server, or check for | ||||||
|  |     errors in the logs and try different folder separator values. | ||||||
|  |  | ||||||
| .. note:: | .. note:: | ||||||
|  |  | ||||||
|     Paperless will process the rules in the order defined in the admin page. |     Paperless will process the rules in the order defined in the admin page. | ||||||
|   | |||||||
| @@ -47,24 +47,29 @@ if [[ $(id -u) == "0" ]] ; then | |||||||
| 	exit 1 | 	exit 1 | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [[ -z $(which wget) ]] ; then | if ! command -v wget &> /dev/null ; then | ||||||
| 	echo "wget executable not found. Is wget installed?" | 	echo "wget executable not found. Is wget installed?" | ||||||
| 	exit 1 | 	exit 1 | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [[ -z $(which docker) ]] ; then | if ! command -v docker &> /dev/null ; then | ||||||
| 	echo "docker executable not found. Is docker installed?" | 	echo "docker executable not found. Is docker installed?" | ||||||
| 	exit 1 | 	exit 1 | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [[ -z $(which docker-compose) ]] ; then | DOCKER_COMPOSE_CMD="docker-compose" | ||||||
| 	echo "docker-compose executable not found. Is docker-compose installed?" | if ! command -v ${DOCKER_COMPOSE_CMD} ; then | ||||||
| 	exit 1 | 	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 | fi | ||||||
|  |  | ||||||
| # Check if user has permissions to run Docker by trying to get the status of Docker (docker status). | # Check if user has permissions to run Docker by trying to get the status of Docker (docker status). | ||||||
| # If this fails, the user probably does not have permissions for Docker. | # If this fails, the user probably does not have permissions for Docker. | ||||||
| if [ ! "$(docker stats --no-stream 2>/dev/null 1>&2)" ] ; then | if ! docker stats --no-stream &> /dev/null ; then | ||||||
| 	echo "" | 	echo "" | ||||||
| 	echo "WARN: It look like the current user does not have Docker permissions." | 	echo "WARN: It look like the current user does not have Docker permissions." | ||||||
| 	echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user." | 	echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user." | ||||||
| @@ -87,6 +92,14 @@ echo "" | |||||||
| echo "1. Application configuration" | echo "1. Application configuration" | ||||||
| echo "============================" | echo "============================" | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "The URL paperless will be available at. This is required if the" | ||||||
|  | echo "installation will be accessible via the web, otherwise can be left blank." | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | ask "URL" "" | ||||||
|  | URL=$ask_result | ||||||
|  |  | ||||||
| echo "" | echo "" | ||||||
| echo "The port on which the paperless webserver will listen for incoming" | echo "The port on which the paperless webserver will listen for incoming" | ||||||
| echo "connections." | echo "connections." | ||||||
| @@ -273,6 +286,7 @@ if [[ "$DATABASE_BACKEND" == "postgres" ]] ; then | |||||||
| 	fi | 	fi | ||||||
| fi | fi | ||||||
| echo "" | echo "" | ||||||
|  | echo "URL: $URL" | ||||||
| echo "Port: $PORT" | echo "Port: $PORT" | ||||||
| echo "Database: $DATABASE_BACKEND" | echo "Database: $DATABASE_BACKEND" | ||||||
| echo "Tika enabled: $TIKA_ENABLED" | echo "Tika enabled: $TIKA_ENABLED" | ||||||
| @@ -308,6 +322,9 @@ SECRET_KEY=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1) | |||||||
| DEFAULT_LANGUAGES="deu eng fra ita spa" | DEFAULT_LANGUAGES="deu eng fra ita spa" | ||||||
|  |  | ||||||
| { | { | ||||||
|  | 	if [[ ! $URL == "" ]] ; then | ||||||
|  | 		echo "PAPERLESS_URL=$URL" | ||||||
|  | 	fi | ||||||
| 	if [[ ! $USERMAP_UID == "1000" ]] ; then | 	if [[ ! $USERMAP_UID == "1000" ]] ; then | ||||||
| 		echo "USERMAP_UID=$USERMAP_UID" | 		echo "USERMAP_UID=$USERMAP_UID" | ||||||
| 	fi | 	fi | ||||||
| @@ -351,8 +368,8 @@ if [ "$l1" -eq "$l2" ] ; then | |||||||
| fi | fi | ||||||
|  |  | ||||||
|  |  | ||||||
| docker-compose pull | ${DOCKER_COMPOSE_CMD} pull | ||||||
|  |  | ||||||
| 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 -d | ${DOCKER_COMPOSE_CMD} up -d | ||||||
|   | |||||||
| @@ -27,8 +27,10 @@ | |||||||
| # Security and hosting | # Security and hosting | ||||||
|  |  | ||||||
| #PAPERLESS_SECRET_KEY=change-me | #PAPERLESS_SECRET_KEY=change-me | ||||||
| #PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com | #PAPERLESS_URL=https://example.com | ||||||
| #PAPERLESS_CORS_ALLOWED_HOSTS=http://example.com,http://localhost:8000 | #PAPERLESS_CSRF_TRUSTED_ORIGINS=https://example.com # can be set using PAPERLESS_URL | ||||||
|  | #PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com # can be set using PAPERLESS_URL | ||||||
|  | #PAPERLESS_CORS_ALLOWED_HOSTS=https://localhost:8080,https://example.com # can be set using PAPERLESS_URL | ||||||
| #PAPERLESS_FORCE_SCRIPT_NAME= | #PAPERLESS_FORCE_SCRIPT_NAME= | ||||||
| #PAPERLESS_STATIC_URL=/static/ | #PAPERLESS_STATIC_URL=/static/ | ||||||
| #PAPERLESS_AUTO_LOGIN_USERNAME= | #PAPERLESS_AUTO_LOGIN_USERNAME= | ||||||
| @@ -58,8 +60,10 @@ | |||||||
| #PAPERLESS_CONSUMER_POLLING=10 | #PAPERLESS_CONSUMER_POLLING=10 | ||||||
| #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false | #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false | ||||||
| #PAPERLESS_CONSUMER_RECURSIVE=false | #PAPERLESS_CONSUMER_RECURSIVE=false | ||||||
| #PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*"] | #PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"] | ||||||
| #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false | #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false | ||||||
|  | #PAPERLESS_CONSUMER_ENABLE_BARCODES=false | ||||||
|  | #PAPERLESS_CONSUMER_ENABLE_BARCODES=PATCHT | ||||||
| #PAPERLESS_OPTIMIZE_THUMBNAILS=true | #PAPERLESS_OPTIMIZE_THUMBNAILS=true | ||||||
| #PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | #PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | ||||||
| #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | ||||||
| @@ -67,6 +71,7 @@ | |||||||
| #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] | #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] | ||||||
| #PAPERLESS_THUMBNAIL_FONT_NAME= | #PAPERLESS_THUMBNAIL_FONT_NAME= | ||||||
| #PAPERLESS_IGNORE_DATES= | #PAPERLESS_IGNORE_DATES= | ||||||
|  | #PAPERLESS_ENABLE_UPDATE_CHECK= | ||||||
|  |  | ||||||
| # Tika settings | # Tika settings | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,15 +5,15 @@ | |||||||
| #    pipenv lock --requirements | #    pipenv lock --requirements | ||||||
| # | # | ||||||
|  |  | ||||||
| -i https://pypi.python.org/simple | -i https://pypi.python.org/simple/ | ||||||
| --extra-index-url https://www.piwheels.org/simple | --extra-index-url https://www.piwheels.org/simple/ | ||||||
| aioredis==1.3.1 | aioredis==1.3.1 | ||||||
| anyio==3.5.0; python_full_version >= '3.6.2' | anyio==3.5.0; python_full_version >= '3.6.2' | ||||||
| arrow==1.2.2; python_version >= '3.6' | arrow==1.2.2; python_version >= '3.6' | ||||||
| asgiref==3.5.0; python_version >= '3.7' | asgiref==3.5.0; python_version >= '3.7' | ||||||
| async-timeout==4.0.2; python_version >= '3.6' | async-timeout==4.0.2; python_version >= '3.6' | ||||||
| attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| autobahn==22.2.2; python_version >= '3.7' | autobahn==22.3.2; python_version >= '3.7' | ||||||
| automat==20.2.0 | automat==20.2.0 | ||||||
| backports.zoneinfo==0.2.1; python_version < '3.9' | backports.zoneinfo==0.2.1; python_version < '3.9' | ||||||
| blessed==1.19.1; python_version >= '2.7' | blessed==1.19.1; python_version >= '2.7' | ||||||
| @@ -23,7 +23,7 @@ channels-redis==3.4.0 | |||||||
| channels==3.0.4 | channels==3.0.4 | ||||||
| chardet==4.0.0; python_version >= '3.1' | chardet==4.0.0; python_version >= '3.1' | ||||||
| charset-normalizer==2.0.12; python_version >= '3' | charset-normalizer==2.0.12; python_version >= '3' | ||||||
| click==8.0.4; python_version >= '3.6' | click==8.1.2; python_version >= '3.7' | ||||||
| coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| concurrent-log-handler==0.9.20 | concurrent-log-handler==0.9.20 | ||||||
| constantly==15.1.0 | constantly==15.1.0 | ||||||
| @@ -35,7 +35,7 @@ django-extensions==3.1.5 | |||||||
| django-filter==21.1 | django-filter==21.1 | ||||||
| django-picklefield==3.0.1; python_version >= '3' | django-picklefield==3.0.1; python_version >= '3' | ||||||
| django-q==1.3.9 | django-q==1.3.9 | ||||||
| django==4.0.3 | django==4.0.4 | ||||||
| djangorestframework==3.13.1 | djangorestframework==3.13.1 | ||||||
| filelock==3.6.0 | filelock==3.6.0 | ||||||
| fuzzywuzzy[speedup]==0.18.0 | fuzzywuzzy[speedup]==0.18.0 | ||||||
| @@ -47,8 +47,8 @@ humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1 | |||||||
| hyperlink==21.0.0 | hyperlink==21.0.0 | ||||||
| idna==3.3; python_version >= '3.5' | idna==3.3; python_version >= '3.5' | ||||||
| imap-tools==0.53.0 | imap-tools==0.53.0 | ||||||
| img2pdf==0.4.3 | img2pdf==0.4.4 | ||||||
| importlib-resources==5.4.0; python_version < '3.9' | importlib-resources==5.6.0; python_version < '3.9' | ||||||
| incremental==21.3.0 | incremental==21.3.0 | ||||||
| inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||||
| inotifyrecursive==0.3.5 | inotifyrecursive==0.3.5 | ||||||
| @@ -57,55 +57,57 @@ langdetect==1.0.9 | |||||||
| lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| msgpack==1.0.3 | msgpack==1.0.3 | ||||||
| numpy==1.22.3; python_version >= '3.8' | numpy==1.22.3; python_version >= '3.8' | ||||||
| ocrmypdf==13.4.1 | ocrmypdf==13.4.2 | ||||||
| packaging==21.3; python_version >= '3.6' | packaging==21.3; python_version >= '3.6' | ||||||
| pathvalidate==2.5.0 | pathvalidate==2.5.0 | ||||||
| pdfminer.six==20211012 | pdf2image==1.16.0 | ||||||
| pikepdf==5.1.0 | pdfminer.six==20220319 | ||||||
| pillow==9.0.1 | pikepdf==5.1.1 | ||||||
|  | pillow==9.1.0 | ||||||
| pluggy==1.0.0; python_version >= '3.6' | pluggy==1.0.0; python_version >= '3.6' | ||||||
| portalocker==2.4.0; python_version >= '3' | portalocker==2.4.0; python_version >= '3' | ||||||
| psycopg2-binary==2.9.3 | psycopg2==2.9.3 | ||||||
| pyasn1-modules==0.2.8 | pyasn1-modules==0.2.8 | ||||||
| pyasn1==0.4.8 | pyasn1==0.4.8 | ||||||
| pycparser==2.21 | pycparser==2.21 | ||||||
| pyopenssl==22.0.0 | pyopenssl==22.0.0 | ||||||
| pyparsing==3.0.7; python_version >= '3.6' | pyparsing==3.0.8; python_full_version >= '3.6.8' | ||||||
| python-dateutil==2.8.2 | python-dateutil==2.8.2 | ||||||
| python-dotenv==0.19.2 | python-dotenv==0.20.0 | ||||||
| python-gnupg==0.4.8 | python-gnupg==0.4.8 | ||||||
| python-levenshtein==0.12.2 | python-levenshtein==0.12.2 | ||||||
| python-magic==0.4.25 | python-magic==0.4.25 | ||||||
| pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | ||||||
| pytz==2022.1 | pytz==2022.1 | ||||||
| pyyaml==6.0 | pyyaml==6.0 | ||||||
|  | pyzbar==0.1.9 | ||||||
| redis==3.5.3 | redis==3.5.3 | ||||||
| regex==2022.3.2; python_version >= '3.6' | regex==2022.3.2; python_version >= '3.6' | ||||||
| reportlab==3.6.8; python_version >= '3.6' and python_version < '4' | reportlab==3.6.9; python_version >= '3.7' and python_version < '4' | ||||||
| requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | ||||||
| scikit-learn==1.0.2 | scikit-learn==1.0.2 | ||||||
| scipy==1.8.0; python_version < '3.11' and python_version >= '3.8' | scipy==1.8.0; python_version < '3.11' and python_version >= '3.8' | ||||||
| service-identity==21.1.0 | service-identity==21.1.0 | ||||||
| setuptools==60.10.0; python_version >= '3.7' | setuptools==62.1.0; python_version >= '3.7' | ||||||
| six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||||
| sniffio==1.2.0; python_version >= '3.5' | sniffio==1.2.0; python_version >= '3.5' | ||||||
| sqlparse==0.4.2; python_version >= '3.5' | sqlparse==0.4.2; python_version >= '3.5' | ||||||
| threadpoolctl==3.1.0; python_version >= '3.6' | threadpoolctl==3.1.0; python_version >= '3.6' | ||||||
| tika==1.24 | tika==1.24 | ||||||
| tqdm==4.63.0 | tqdm==4.64.0 | ||||||
| twisted[tls]==22.2.0; python_full_version >= '3.6.7' | twisted[tls]==22.4.0; python_full_version >= '3.6.7' | ||||||
| txaio==22.2.1; python_version >= '3.6' | txaio==22.2.1; python_version >= '3.6' | ||||||
| typing-extensions==4.1.1; python_version >= '3.6' | typing-extensions==4.1.1; python_version >= '3.6' | ||||||
| tzdata==2022.1; python_version >= '3.6' | tzdata==2022.1; python_version >= '3.6' | ||||||
| tzlocal==4.1; python_version >= '3.6' | tzlocal==4.2; python_version >= '3.6' | ||||||
| urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' | urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' | ||||||
| uvicorn[standard]==0.17.6 | uvicorn[standard]==0.17.6 | ||||||
| uvloop==0.16.0 | uvloop==0.16.0 | ||||||
| watchdog==2.1.6 | watchdog==2.1.7 | ||||||
| watchgod==0.8.1 | watchgod==0.8.2 | ||||||
| wcwidth==0.2.5 | wcwidth==0.2.5 | ||||||
| websockets==10.2 | websockets==10.2 | ||||||
| whitenoise==6.0.0 | whitenoise==6.0.0 | ||||||
| whoosh==2.7.4 | whoosh==2.7.4 | ||||||
| zipp==3.7.0; python_version < '3.9' | zipp==3.8.0; python_version < '3.9' | ||||||
| zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								src-ui/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -45,4 +45,7 @@ testem.log | |||||||
| # System Files | # System Files | ||||||
| .DS_Store | .DS_Store | ||||||
| Thumbs.db | Thumbs.db | ||||||
|  |  | ||||||
|  | # Cypress | ||||||
| cypress/videos/**/* | cypress/videos/**/* | ||||||
|  | cypress/screenshots/**/* | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ | |||||||
| 			"i18n": { | 			"i18n": { | ||||||
| 				"sourceLocale": "en-US", | 				"sourceLocale": "en-US", | ||||||
| 				"locales": { | 				"locales": { | ||||||
|  | 					"be-BY": "src/locale/messages.be_BY.xlf", | ||||||
| 					"cs-CZ": "src/locale/messages.cs_CZ.xlf", | 					"cs-CZ": "src/locale/messages.cs_CZ.xlf", | ||||||
| 					"da-DK": "src/locale/messages.da_DK.xlf", | 					"da-DK": "src/locale/messages.da_DK.xlf", | ||||||
| 					"de-DE": "src/locale/messages.de_DE.xlf", | 					"de-DE": "src/locale/messages.de_DE.xlf", | ||||||
|   | |||||||
| @@ -0,0 +1 @@ | |||||||
|  | {"count":27,"next":"http://localhost:8000/api/correspondents/?page=2","previous":null,"results":[{"id":9,"slug":"abc-test-correspondent","name":"ABC Test Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":13,"slug":"corresp-10","name":"Corresp 10","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":14,"slug":"corresp-11","name":"Corresp 11","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":15,"slug":"corresp-12","name":"Corresp 12","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":16,"slug":"corresp-13","name":"Corresp 13","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":18,"slug":"corresp-15","name":"Corresp 15","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":19,"slug":"corresp-16","name":"Corresp 16","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":20,"slug":"corresp-17","name":"Corresp 17","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":21,"slug":"corresp-18","name":"Corresp 18","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":22,"slug":"corresp-19","name":"Corresp 19","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":23,"slug":"corresp-20","name":"Corresp 20","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":24,"slug":"corresp-21","name":"Corresp 21","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":25,"slug":"corresp-22","name":"Corresp 22","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":26,"slug":"corresp-23","name":"Corresp 23","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":5,"slug":"corresp-3","name":"Corresp 3","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":6,"slug":"corresp-4","name":"Corresp 4","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":7,"slug":"corresp-5","name":"Corresp 5","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":8,"slug":"corresp-6","name":"Corresp 6","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":10,"slug":"corresp-7","name":"Corresp 7","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":11,"slug":"corresp-8","name":"Corresp 8","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":12,"slug":"corresp-9","name":"Corresp 9","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":17,"slug":"correspondent-14","name":"Correspondent 14","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":2,"slug":"correspondent-2","name":"Correspondent 2","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":7,"last_correspondence":"2021-01-20T23:37:58.204614Z"},{"id":27,"slug":"michael-shamoon","name":"Michael Shamoon","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2022-03-16T03:48:50.089624Z"},{"id":4,"slug":"newest-correspondent","name":"Newest Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2021-02-07T08:00:00Z"}]} | ||||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/document_types/doctypes.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | {"count":1,"next":null,"previous":null,"results":[{"id":1,"slug":"test","name":"Test Doc Type","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0}]} | ||||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/documents/1/metadata.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | {"original_checksum":"e959bc7d593245d92685213264e962ba","original_size":963754,"original_mime_type":"application/pdf","media_filename":"2022/lorem-ipsum.pdf","has_archive_version":true,"original_metadata":[],"archive_checksum":"5a1f46a9150bcade978c764b039ce4d0","archive_media_filename":"2022/lorem-ipsum.pdf","archive_size":351160,"archive_metadata":[{"namespace":"http://ns.adobe.com/pdf/1.3/","prefix":"pdf","key":"Producer","value":"pikepdf5.0.1"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"ModifyDate","value":"2022-03-22T04:53:18+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreateDate","value":"2022-03-22T18:05:43+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreatorTool","value":"ocrmypdf13.4.0/TesseractOCR-PDF4.1.1"},{"namespace":"http://ns.adobe.com/xap/1.0/mm/","prefix":"xmpMM","key":"DocumentID","value":"uuid:df27edcf-e34a-11f7-0000-8fa6067a3c04"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"format","value":"application/pdf"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"title","value":"ScannedDocument"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"part","value":"2"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"conformance","value":"B"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"creator","value":"None"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"MetadataDate","value":"2022-03-22T21:53:18.882551-07:00"}]} | ||||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/documents/1/suggestions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | {"correspondents":[],"tags":[3],"document_types":[1]} | ||||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/saved_views/savedviews.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | {"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]} | ||||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/tags/tags.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | {"count":8,"next":null,"previous":null,"results":[{"id":4,"slug":"another-sample-tag","name":"Another Sample Tag","color":"#a6cee3","text_color":"#000000","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":7,"slug":"newone","name":"NewOne","color":"#9e4ad1","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":6,"slug":"partial-tag","name":"Partial Tag","color":"#72dba7","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":1},{"id":2,"slug":"tag-2","name":"Tag 2","color":"#612db7","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":3,"slug":"tag-3","name":"Tag 3","color":"#b2df8a","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4},{"id":5,"slug":"tagwithpartial","name":"TagWithPartial","color":"#3b2db4","text_color":"#ffffff","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":8,"slug":"test-another","name":"Test Another","color":"#3ccea5","text_color":"#000000","match":"","matching_algorithm":4,"is_insensitive":true,"is_inbox_tag":false,"document_count":0},{"id":1,"slug":"test-tag","name":"Test Tag","color":"#fb9a99","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4}]} | ||||||
							
								
								
									
										64
									
								
								src-ui/cypress/integration/document-detail.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | |||||||
|  | describe('document-detail', () => { | ||||||
|  |   beforeEach(() => { | ||||||
|  |     this.modifiedDocuments = [] | ||||||
|  |  | ||||||
|  |     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
|  |       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||||
|  |         let response = { ...documentsJson } | ||||||
|  |         response = response.results.find((d) => d.id == 1) | ||||||
|  |         req.reply(response) | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('PUT', 'http://localhost:8000/api/documents/1/', (req) => { | ||||||
|  |       this.modifiedDocuments.push(req.body) // store this for later | ||||||
|  |       req.reply({ result: 'OK' }) | ||||||
|  |     }).as('saveDoc') | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/documents/1/metadata/', { | ||||||
|  |       fixture: 'documents/1/metadata.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { | ||||||
|  |       fixture: 'documents/1/suggestions.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/saved_views/*', { | ||||||
|  |       fixture: 'saved_views/savedviews.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/tags/*', { | ||||||
|  |       fixture: 'tags/tags.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/correspondents/*', { | ||||||
|  |       fixture: 'correspondents/correspondents.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/document_types/*', { | ||||||
|  |       fixture: 'document_types/doctypes.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.viewport(1024, 1024) | ||||||
|  |     cy.visit('/documents/1/') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should activate / deactivate save button when changes are saved', () => { | ||||||
|  |     cy.contains('button', 'Save').should('be.disabled') | ||||||
|  |     cy.get('app-input-text[formcontrolname="title"]') | ||||||
|  |       .type(' additional') | ||||||
|  |       .wait(1500) // this delay is for frontend debounce | ||||||
|  |     cy.contains('button', 'Save').should('not.be.disabled') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should warn on unsaved changes', () => { | ||||||
|  |     cy.get('app-input-text[formcontrolname="title"]') | ||||||
|  |       .type(' additional') | ||||||
|  |       .wait(1500) // this delay is for frontend debounce | ||||||
|  |     cy.get('button[title="Close"]').click() | ||||||
|  |     cy.contains('You have unsaved changes') | ||||||
|  |     cy.contains('button', 'Cancel').click().wait(150) | ||||||
|  |     cy.contains('button', 'Save').click().wait('@saveDoc').wait(2000) // navigates away after saving | ||||||
|  |     cy.contains('You have unsaved changes').should('not.exist') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @@ -1,20 +1,143 @@ | |||||||
| describe('documents-list', () => { | describe('documents-list', () => { | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|     cy.intercept('http://localhost:8000/api/documents/*', { |     this.bulkEdits = {} | ||||||
|       fixture: 'documents/documents.json', |  | ||||||
|     }); |     // mock API methods | ||||||
|  |     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
|  |       // bulk edit | ||||||
|  |       cy.intercept( | ||||||
|  |         'POST', | ||||||
|  |         'http://localhost:8000/api/documents/bulk_edit/', | ||||||
|  |         (req) => { | ||||||
|  |           this.bulkEdits = req.body // store this for later | ||||||
|  |           req.reply({ result: 'OK' }) | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |  | ||||||
|  |       cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => { | ||||||
|  |         let response = { ...documentsJson } | ||||||
|  |  | ||||||
|  |         // bulkEdits was set earlier by bulk_edit intercept | ||||||
|  |         if (this.bulkEdits.hasOwnProperty('documents')) { | ||||||
|  |           response.results = response.results.map((d) => { | ||||||
|  |             if ((this.bulkEdits['documents'] as Array<number>).includes(d.id)) { | ||||||
|  |               switch (this.bulkEdits['method']) { | ||||||
|  |                 case 'modify_tags': | ||||||
|  |                   d.tags = (d.tags as Array<number>).concat([ | ||||||
|  |                     this.bulkEdits['parameters']['add_tags'], | ||||||
|  |                   ]) | ||||||
|  |                   break | ||||||
|  |                 case 'set_correspondent': | ||||||
|  |                   d.correspondent = | ||||||
|  |                     this.bulkEdits['parameters']['correspondent'] | ||||||
|  |                   break | ||||||
|  |                 case 'set_document_type': | ||||||
|  |                   d.document_type = | ||||||
|  |                     this.bulkEdits['parameters']['document_type'] | ||||||
|  |                   break | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return d | ||||||
|  |           }) | ||||||
|  |         } else if (req.query.hasOwnProperty('tags__id__all')) { | ||||||
|  |           // filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&tags__id__all=2 | ||||||
|  |           const tag_id = +req.query['tags__id__all'] | ||||||
|  |           response.results = (documentsJson.results as Array<any>).filter((d) => | ||||||
|  |             (d.tags as Array<number>).includes(tag_id) | ||||||
|  |           ) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         req.reply(response) | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|     cy.intercept('http://localhost:8000/api/documents/1/thumb/', { |     cy.intercept('http://localhost:8000/api/documents/1/thumb/', { | ||||||
|       fixture: 'documents/lorem-ipsum.png', |       fixture: 'documents/lorem-ipsum.png', | ||||||
|     }); |     }) | ||||||
|  |  | ||||||
|     cy.visit('/documents'); |     cy.intercept('http://localhost:8000/api/tags/*', { | ||||||
|   }); |       fixture: 'tags/tags.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/correspondents/*', { | ||||||
|  |       fixture: 'correspondents/correspondents.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/document_types/*', { | ||||||
|  |       fixture: 'document_types/doctypes.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.visit('/documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|   it('should show a list of documents rendered as cards with thumbnails', () => { |   it('should show a list of documents rendered as cards with thumbnails', () => { | ||||||
|     cy.contains('One document'); |     cy.contains('3 documents') | ||||||
|     cy.contains('lorem-ipsum'); |     cy.contains('lorem-ipsum') | ||||||
|     cy.get('app-document-card-small:first-of-type img') |     cy.get('app-document-card-small:first-of-type img') | ||||||
|       .invoke('attr', 'src') |       .invoke('attr', 'src') | ||||||
|       .should('eq', 'http://localhost:8000/api/documents/1/thumb/'); |       .should('eq', 'http://localhost:8000/api/documents/1/thumb/') | ||||||
|   }); |   }) | ||||||
| }); |  | ||||||
|  |   it('should change to table "details" view', () => { | ||||||
|  |     cy.get('div.btn-group-toggle input[value="details"]').parent().click() | ||||||
|  |     cy.get('table') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should change to large cards view', () => { | ||||||
|  |     cy.get('div.btn-group-toggle input[value="largeCards"]').parent().click() | ||||||
|  |     cy.get('app-document-card-large') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should filter tags', () => { | ||||||
|  |     cy.get('app-filter-editor app-filterable-dropdown[title="Tags"]').within( | ||||||
|  |       () => { | ||||||
|  |         cy.contains('button', 'Tags').click() | ||||||
|  |         cy.contains('button', 'Tag 2').click() | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should apply tags', () => { | ||||||
|  |     cy.get('app-document-card-small:first-of-type').click() | ||||||
|  |     cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within( | ||||||
|  |       () => { | ||||||
|  |         cy.contains('button', 'Tags').click() | ||||||
|  |         cy.contains('button', 'Test Tag').click() | ||||||
|  |         cy.contains('button', 'Apply').click() | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |     cy.contains('button', 'Confirm').click() | ||||||
|  |     cy.get('app-document-card-small:first-of-type').contains('Test Tag') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should apply correspondent', () => { | ||||||
|  |     cy.get('app-document-card-small:first-of-type').click() | ||||||
|  |     cy.get( | ||||||
|  |       'app-bulk-editor app-filterable-dropdown[title="Correspondent"]' | ||||||
|  |     ).within(() => { | ||||||
|  |       cy.contains('button', 'Correspondent').click() | ||||||
|  |       cy.contains('button', 'ABC Test Correspondent').click() | ||||||
|  |       cy.contains('button', 'Apply').click() | ||||||
|  |     }) | ||||||
|  |     cy.contains('button', 'Confirm').click() | ||||||
|  |     cy.get('app-document-card-small:first-of-type').contains( | ||||||
|  |       'ABC Test Correspondent' | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should apply document type', () => { | ||||||
|  |     cy.get('app-document-card-small:first-of-type').click() | ||||||
|  |     cy.get( | ||||||
|  |       'app-bulk-editor app-filterable-dropdown[title="Document type"]' | ||||||
|  |     ).within(() => { | ||||||
|  |       cy.contains('button', 'Document type').click() | ||||||
|  |       cy.contains('button', 'Test Doc Type').click() | ||||||
|  |       cy.contains('button', 'Apply').click() | ||||||
|  |     }) | ||||||
|  |     cy.contains('button', 'Confirm').click() | ||||||
|  |     cy.get('app-document-card-small:first-of-type').contains('Test Doc Type') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src-ui/cypress/integration/manage.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | |||||||
|  | describe('manage', () => { | ||||||
|  |   beforeEach(() => { | ||||||
|  |     cy.intercept('http://localhost:8000/api/correspondents/*', { | ||||||
|  |       fixture: 'correspondents/correspondents.json', | ||||||
|  |     }) | ||||||
|  |     cy.intercept('http://localhost:8000/api/tags/*', { | ||||||
|  |       fixture: 'tags/tags.json', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of correspondents with bottom pagination as well', () => { | ||||||
|  |     cy.visit('/correspondents') | ||||||
|  |     cy.get('tbody').find('tr').its('length').should('eq', 25) | ||||||
|  |     cy.get('ngb-pagination').its('length').should('eq', 2) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of tags without bottom pagination', () => { | ||||||
|  |     cy.visit('/tags') | ||||||
|  |     cy.get('tbody').find('tr').its('length').should('eq', 8) | ||||||
|  |     cy.get('ngb-pagination').its('length').should('eq', 1) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by tag', () => { | ||||||
|  |     cy.intercept('http://localhost:8000/api/documents/*', (req) => { | ||||||
|  |       if (req.url.indexOf('tags__id__all=4')) | ||||||
|  |         req.reply({ count: 3, next: null, previous: null, results: [] }) | ||||||
|  |     }) | ||||||
|  |     cy.visit('/tags') | ||||||
|  |     cy.get('tbody').find('button').contains('Documents').first().click() // id = 4 | ||||||
|  |     cy.contains('3 documents') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										91
									
								
								src-ui/cypress/integration/settings.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,91 @@ | |||||||
|  | describe('settings', () => { | ||||||
|  |   beforeEach(() => { | ||||||
|  |     this.modifiedViews = [] | ||||||
|  |  | ||||||
|  |     // mock API methods | ||||||
|  |     cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { | ||||||
|  |       // saved views PATCH | ||||||
|  |       cy.intercept( | ||||||
|  |         'PATCH', | ||||||
|  |         'http://localhost:8000/api/saved_views/*', | ||||||
|  |         (req) => { | ||||||
|  |           this.modifiedViews.push(req.body) // store this for later | ||||||
|  |           req.reply({ result: 'OK' }) | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |  | ||||||
|  |       cy.intercept('GET', 'http://localhost:8000/api/saved_views/*', (req) => { | ||||||
|  |         let response = { ...savedViewsJson } | ||||||
|  |         if (this.modifiedViews.length) { | ||||||
|  |           response.results = response.results.map((v) => { | ||||||
|  |             if (this.modifiedViews.find((mv) => mv.id == v.id)) | ||||||
|  |               v = this.modifiedViews.find((mv) => mv.id == v.id) | ||||||
|  |             return v | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         req.reply(response) | ||||||
|  |       }).as('savedViews') | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
|  |       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||||
|  |         let response = { ...documentsJson } | ||||||
|  |         response = response.results.find((d) => d.id == 1) | ||||||
|  |         req.reply(response) | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/documents/1/metadata/', { | ||||||
|  |       fixture: 'documents/1/metadata.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { | ||||||
|  |       fixture: 'documents/1/suggestions.json', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.viewport(1024, 1024) | ||||||
|  |     cy.visit('/settings') | ||||||
|  |     cy.wait('@savedViews') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should activate / deactivate save button when settings change and are saved', () => { | ||||||
|  |     cy.contains('button', 'Save').should('be.disabled') | ||||||
|  |     cy.contains('Use system settings').click() | ||||||
|  |     cy.contains('button', 'Save').should('not.be.disabled') | ||||||
|  |     cy.contains('button', 'Save').click() | ||||||
|  |     cy.contains('button', 'Save').should('be.disabled') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should warn on unsaved changes', () => { | ||||||
|  |     cy.contains('Use system settings').click() | ||||||
|  |     cy.contains('a', 'Dashboard').click() | ||||||
|  |     cy.contains('You have unsaved changes') | ||||||
|  |     cy.contains('button', 'Cancel').click() | ||||||
|  |     cy.contains('button', 'Save').click().wait('@savedViews') | ||||||
|  |     cy.contains('a', 'Dashboard').click() | ||||||
|  |     cy.contains('You have unsaved changes').should('not.exist') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should apply appearance changes when set', () => { | ||||||
|  |     cy.contains('Use system settings').click() | ||||||
|  |     cy.get('body').should('not.have.class', 'color-scheme-system') | ||||||
|  |     cy.contains('Enable dark mode').click() | ||||||
|  |     cy.get('body').should('have.class', 'color-scheme-dark') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should remove saved view from sidebar when unset', () => { | ||||||
|  |     cy.contains('a', 'Saved views').click() | ||||||
|  |     cy.get('#show_in_sidebar_1').click() | ||||||
|  |     cy.contains('button', 'Save').click().wait('@savedViews') | ||||||
|  |     cy.contains('li', 'Inbox').should('not.exist') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should remove saved view from dashboard when unset', () => { | ||||||
|  |     cy.contains('a', 'Saved views').click() | ||||||
|  |     cy.get('#show_on_dashboard_1').click() | ||||||
|  |     cy.contains('button', 'Save').click().wait('@savedViews') | ||||||
|  |     cy.visit('/dashboard') | ||||||
|  |     cy.get('app-saved-view-widget').contains('Inbox').should('not.exist') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										5910
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -13,26 +13,24 @@ | |||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular/animations": "~13.2.6", |     "@angular/common": "~13.3.1", | ||||||
|     "@angular/common": "~13.2.6", |     "@angular/compiler": "~13.3.1", | ||||||
|     "@angular/compiler": "~13.3.0", |     "@angular/core": "~13.3.1", | ||||||
|     "@angular/core": "~13.3.0", |     "@angular/forms": "~13.3.1", | ||||||
|     "@angular/forms": "~13.2.6", |     "@angular/localize": "~13.3.1", | ||||||
|     "@angular/localize": "~13.2.6", |     "@angular/platform-browser": "~13.3.1", | ||||||
|     "@angular/platform-browser": "~13.3.0", |     "@angular/platform-browser-dynamic": "~13.3.1", | ||||||
|     "@angular/platform-browser-dynamic": "~13.3.0", |     "@angular/router": "~13.3.1", | ||||||
|     "@angular/router": "~13.2.6", |  | ||||||
|     "@ng-bootstrap/ng-bootstrap": "^12.0.1", |     "@ng-bootstrap/ng-bootstrap": "^12.0.1", | ||||||
|     "@ng-select/ng-select": "^8.1.1", |     "@ng-select/ng-select": "^8.1.1", | ||||||
|     "@ngneat/dirty-check-forms": "^2.0.0", |     "@ngneat/dirty-check-forms": "^3.0.2", | ||||||
|     "@popperjs/core": "^2.11.4", |     "@popperjs/core": "^2.11.4", | ||||||
|     "bootstrap": "^5.1.3", |     "bootstrap": "^5.1.3", | ||||||
|     "file-saver": "^2.0.5", |     "file-saver": "^2.0.5", | ||||||
|     "ng2-pdf-viewer": "^8.0.1", |     "ng2-pdf-viewer": "^9.0.0", | ||||||
|     "ngx-color": "^7.3.3", |     "ngx-color": "^7.3.3", | ||||||
|     "ngx-cookie-service": "^13.1.2", |     "ngx-cookie-service": "^13.1.2", | ||||||
|     "ngx-file-drop": "^13.0.0", |     "ngx-file-drop": "^13.0.0", | ||||||
|     "ngx-infinite-scroll": "^10.0.1", |  | ||||||
|     "rxjs": "~7.5.5", |     "rxjs": "~7.5.5", | ||||||
|     "tslib": "^2.3.1", |     "tslib": "^2.3.1", | ||||||
|     "uuid": "^8.3.1", |     "uuid": "^8.3.1", | ||||||
| @@ -40,21 +38,21 @@ | |||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@angular-builders/jest": "13.0.3", |     "@angular-builders/jest": "13.0.3", | ||||||
|     "@angular-devkit/build-angular": "~13.2.5", |     "@angular-devkit/build-angular": "~13.3.1", | ||||||
|     "@angular/cli": "~13.2.5", |     "@angular/cli": "~13.3.1", | ||||||
|     "@angular/compiler-cli": "~13.2.4", |     "@angular/compiler-cli": "~13.3.1", | ||||||
|     "@types/jest": "27.4.1", |     "@types/jest": "27.4.1", | ||||||
|     "@types/node": "^17.0.21", |     "@types/node": "^17.0.23", | ||||||
|     "codelyzer": "^6.0.2", |     "codelyzer": "^6.0.2", | ||||||
|     "concurrently": "7.0.0", |     "concurrently": "7.0.0", | ||||||
|     "jest": "27.5.1", |     "jest": "27.5.1", | ||||||
|     "ts-node": "~10.7.0", |     "ts-node": "~10.7.0", | ||||||
|     "tslint": "~6.1.3", |     "tslint": "~6.1.3", | ||||||
|     "typescript": "~4.5.5", |     "typescript": "~4.6.3", | ||||||
|     "wait-on": "~6.0.1" |     "wait-on": "~6.0.1" | ||||||
|   }, |   }, | ||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "cypress": "~9.5.2", |     "cypress": "~9.5.3", | ||||||
|     "@cypress/schematic": "^1.6.0" |     "@cypress/schematic": "^1.6.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,13 @@ | |||||||
| <app-toasts></app-toasts> | <app-toasts></app-toasts> | ||||||
|  |  | ||||||
| <router-outlet></router-outlet> | <ngx-file-drop dropZoneClassName="main-dropzone" contentClassName="main-content" [disabled]="!dragDropEnabled" | ||||||
|  | (onFileDrop)="dropped($event)" (onFileOver)="fileOver()" (onFileLeave)="fileLeave()"> | ||||||
|  |     <ng-template ngx-file-drop-content-tmp> | ||||||
|  |         <div class="global-dropzone-overlay fade" [class.show]="fileIsOver" [class.hide]="hidden"> | ||||||
|  |             <h2 i18n>Drop files to begin upload</h2> | ||||||
|  |         </div> | ||||||
|  |         <div [class.inert]="fileIsOver"> | ||||||
|  |             <router-outlet></router-outlet> | ||||||
|  |         </div> | ||||||
|  |     </ng-template> | ||||||
|  | </ngx-file-drop> | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ import { Router } from '@angular/router' | |||||||
| import { Subscription } from 'rxjs' | import { Subscription } from 'rxjs' | ||||||
| import { ConsumerStatusService } from './services/consumer-status.service' | import { ConsumerStatusService } from './services/consumer-status.service' | ||||||
| import { ToastService } from './services/toast.service' | import { ToastService } from './services/toast.service' | ||||||
|  | import { NgxFileDropEntry } from 'ngx-file-drop' | ||||||
|  | import { UploadDocumentsService } from './services/upload-documents.service' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-root', |   selector: 'app-root', | ||||||
| @@ -15,11 +17,16 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|   successSubscription: Subscription |   successSubscription: Subscription | ||||||
|   failedSubscription: Subscription |   failedSubscription: Subscription | ||||||
|  |  | ||||||
|  |   private fileLeaveTimeoutID: any | ||||||
|  |   fileIsOver: boolean = false | ||||||
|  |   hidden: boolean = true | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     private settings: SettingsService, |     private settings: SettingsService, | ||||||
|     private consumerStatusService: ConsumerStatusService, |     private consumerStatusService: ConsumerStatusService, | ||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     private router: Router |     private router: Router, | ||||||
|  |     private uploadDocumentsService: UploadDocumentsService | ||||||
|   ) { |   ) { | ||||||
|     let anyWindow = window as any |     let anyWindow = window as any | ||||||
|     anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js' |     anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js' | ||||||
| @@ -100,4 +107,36 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public get dragDropEnabled(): boolean { | ||||||
|  |     return !this.router.url.includes('dashboard') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public fileOver() { | ||||||
|  |     // allows transition | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.fileIsOver = true | ||||||
|  |     }, 1) | ||||||
|  |     this.hidden = false | ||||||
|  |     // stop fileLeave timeout | ||||||
|  |     clearTimeout(this.fileLeaveTimeoutID) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public fileLeave(immediate: boolean = false) { | ||||||
|  |     const ms = immediate ? 0 : 500 | ||||||
|  |  | ||||||
|  |     this.fileLeaveTimeoutID = setTimeout(() => { | ||||||
|  |       this.fileIsOver = false | ||||||
|  |       // await transition completed | ||||||
|  |       setTimeout(() => { | ||||||
|  |         this.hidden = true | ||||||
|  |       }, 150) | ||||||
|  |     }, ms) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public dropped(files: NgxFileDropEntry[]) { | ||||||
|  |     this.fileLeave(true) | ||||||
|  |     this.uploadDocumentsService.uploadFiles(files) | ||||||
|  |     this.toastService.showInfo($localize`Initiating upload...`, 3000) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -39,7 +39,6 @@ import { TextComponent } from './components/common/input/text/text.component' | |||||||
| import { SelectComponent } from './components/common/input/select/select.component' | import { SelectComponent } from './components/common/input/select/select.component' | ||||||
| import { CheckComponent } from './components/common/input/check/check.component' | import { CheckComponent } from './components/common/input/check/check.component' | ||||||
| import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component' | import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component' | ||||||
| import { InfiniteScrollModule } from 'ngx-infinite-scroll' |  | ||||||
| import { TagsComponent } from './components/common/input/tags/tags.component' | import { TagsComponent } from './components/common/input/tags/tags.component' | ||||||
| import { SortableDirective } from './directives/sortable.directive' | import { SortableDirective } from './directives/sortable.directive' | ||||||
| import { CookieService } from 'ngx-cookie-service' | import { CookieService } from 'ngx-cookie-service' | ||||||
| @@ -69,6 +68,7 @@ import { ColorSliderModule } from 'ngx-color/slider' | |||||||
| import { ColorComponent } from './components/common/input/color/color.component' | import { ColorComponent } from './components/common/input/color/color.component' | ||||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | ||||||
|  |  | ||||||
|  | import localeBe from '@angular/common/locales/be' | ||||||
| import localeCs from '@angular/common/locales/cs' | import localeCs from '@angular/common/locales/cs' | ||||||
| import localeDa from '@angular/common/locales/da' | import localeDa from '@angular/common/locales/da' | ||||||
| import localeDe from '@angular/common/locales/de' | import localeDe from '@angular/common/locales/de' | ||||||
| @@ -88,6 +88,7 @@ import localeSv from '@angular/common/locales/sv' | |||||||
| import localeTr from '@angular/common/locales/tr' | import localeTr from '@angular/common/locales/tr' | ||||||
| import localeZh from '@angular/common/locales/zh' | import localeZh from '@angular/common/locales/zh' | ||||||
|  |  | ||||||
|  | registerLocaleData(localeBe) | ||||||
| registerLocaleData(localeCs) | registerLocaleData(localeCs) | ||||||
| registerLocaleData(localeDa) | registerLocaleData(localeDa) | ||||||
| registerLocaleData(localeDe) | registerLocaleData(localeDe) | ||||||
| @@ -168,7 +169,6 @@ registerLocaleData(localeZh) | |||||||
|     FormsModule, |     FormsModule, | ||||||
|     ReactiveFormsModule, |     ReactiveFormsModule, | ||||||
|     NgxFileDropModule, |     NgxFileDropModule, | ||||||
|     InfiniteScrollModule, |  | ||||||
|     PdfViewerModule, |     PdfViewerModule, | ||||||
|     NgSelectModule, |     NgSelectModule, | ||||||
|     ColorSliderModule, |     ColorSliderModule, | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|   </a> |   </a> | ||||||
|   <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"> |   <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"> | ||||||
|     <form (ngSubmit)="search()" class="form-inline flex-grow-1"> |     <form (ngSubmit)="search()" class="form-inline flex-grow-1"> | ||||||
|       <svg width="1em" height="1em"> |       <svg width="1em" height="1em" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#search"/> |         <use xlink:href="assets/bootstrap-icons.svg#search"/> | ||||||
|       </svg> |       </svg> | ||||||
|       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" |       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|         <span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline"> |         <span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline"> | ||||||
|           {{displayName}} |           {{displayName}} | ||||||
|         </span> |         </span> | ||||||
|         <svg width="1.3em" height="1.3em"> |         <svg width="1.3em" height="1.3em" fill="currentColor"> | ||||||
|           <use xlink:href="assets/bootstrap-icons.svg#person-circle"/> |           <use xlink:href="assets/bootstrap-icons.svg#person-circle"/> | ||||||
|         </svg> |         </svg> | ||||||
|       </button> |       </button> | ||||||
| @@ -62,7 +62,7 @@ | |||||||
|             </a> |             </a> | ||||||
|           </li> |           </li> | ||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()"> |             <a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"> | ||||||
|               <svg class="sidebaricon" fill="currentColor"> |               <svg class="sidebaricon" fill="currentColor"> | ||||||
|                 <use xlink:href="assets/bootstrap-icons.svg#files"/> |                 <use xlink:href="assets/bootstrap-icons.svg#files"/> | ||||||
|               </svg> <ng-container i18n>Documents</ng-container> |               </svg> <ng-container i18n>Documents</ng-container> | ||||||
| @@ -170,21 +170,46 @@ | |||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <div class="d-flex w-100 flex-wrap"> |             <div class="d-flex w-100 flex-wrap"> | ||||||
|               <a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx"> |               <a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx"> | ||||||
|                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16"> |                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon" viewBox="0 0 16 16"> | ||||||
|                   <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> |                   <use xlink:href="assets/bootstrap-icons.svg#github" /> | ||||||
|                 </svg> <ng-container i18n>GitHub</ng-container> |                 </svg> <ng-container i18n>GitHub</ng-container> | ||||||
|               </a> |               </a> | ||||||
|               <a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea"> |               <a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea"> | ||||||
|                 <svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" fill="currentColor" class="bi bi-lightbulb pe-1" viewBox="0 0 16 16"> |                 <svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16"> | ||||||
|                   <path d="M2 6a6 6 0 1 1 10.174 4.31c-.203.196-.359.4-.453.619l-.762 1.769A.5.5 0 0 1 10.5 13a.5.5 0 0 1 0 1 .5.5 0 0 1 0 1l-.224.447a1 1 0 0 1-.894.553H6.618a1 1 0 0 1-.894-.553L5.5 15a.5.5 0 0 1 0-1 .5.5 0 0 1 0-1 .5.5 0 0 1-.46-.302l-.761-1.77a1.964 1.964 0 0 0-.453-.618A5.984 5.984 0 0 1 2 6zm6-5a5 5 0 0 0-3.479 8.592c.263.254.514.564.676.941L5.83 12h4.342l.632-1.467c.162-.377.413-.687.676-.941A5 5 0 0 0 8 1z"/> |                   <use xlink:href="assets/bootstrap-icons.svg#lightbulb" /> | ||||||
|                 </svg> |                 </svg> | ||||||
|                 <ng-container i18n>Suggest an idea</ng-container> |                 <ng-container i18n>Suggest an idea</ng-container> | ||||||
|               </a> |               </a> | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|           <li class="nav-item mt-2"> |           <li class="nav-item mt-2"> | ||||||
|             <div class="px-3 py-2 text-muted small"> |             <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap"> | ||||||
|               {{versionString}} |               <div class="me-3">{{ versionString }}</div> | ||||||
|  |               <div *ngIf="appRemoteVersion" class="version-check"> | ||||||
|  |                 <ng-template #updateAvailablePopContent> | ||||||
|  |                   <span class="small">Paperless-ngx v{{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> | ||||||
|  |                 </ng-template> | ||||||
|  |                 <ng-template #updateCheckingNotEnabledPopContent> | ||||||
|  |                   <span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span> | ||||||
|  |                 </ng-template> | ||||||
|  |                 <ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet"> | ||||||
|  |                   <a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" | ||||||
|  |                   [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||||
|  |                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||||
|  |                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||||
|  |                     </svg> | ||||||
|  |                     <ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> | ||||||
|  |                   </a> | ||||||
|  |                 </ng-container> | ||||||
|  |                 <ng-template #updateCheckNotSet> | ||||||
|  |                   <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking" | ||||||
|  |                   [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||||
|  |                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||||
|  |                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||||
|  |                     </svg> | ||||||
|  |                   </a> | ||||||
|  |                 </ng-template> | ||||||
|  |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|   | |||||||
| @@ -35,20 +35,19 @@ | |||||||
|  |  | ||||||
| .sidebar .nav-link { | .sidebar .nav-link { | ||||||
|   font-weight: 500; |   font-weight: 500; | ||||||
| } |  | ||||||
|  |  | ||||||
| .sidebar .nav-link .sidebaricon { |   &:hover, &.active { | ||||||
|   margin-right: 4px; |     color: var(--bs-primary); | ||||||
| } |   } | ||||||
|  |  | ||||||
| .sidebar .nav-link.active { |   &.active { | ||||||
|   color: var(--bs-primary); |     font-weight: bold; | ||||||
|   font-weight: bold; |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| .sidebar .nav-link.active .sidebaricon, |   .sidebaricon { | ||||||
| .sidebar .nav-link:hover .sidebaricon { |     margin-right: 4px; | ||||||
|   color: inherit; |     color: inherit; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .sidebar-heading { | .sidebar-heading { | ||||||
| @@ -171,8 +170,28 @@ | |||||||
|  |  | ||||||
|     &:focus { |     &:focus { | ||||||
|       background-color: rgba(0, 0, 0, 0.3); |       background-color: rgba(0, 0, 0, 0.3); | ||||||
|  |       color: var(--bs-light); | ||||||
|       flex-grow: 1; |       flex-grow: 1; | ||||||
|       padding-left: 0.5rem; |       padding-left: 0.5rem; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .version-check { | ||||||
|  |   animation: pulse 2s ease-in-out 0s 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes pulse { | ||||||
|  |   0% { | ||||||
|  |     opacity: 0; | ||||||
|  |   } | ||||||
|  |   25% { | ||||||
|  |     opacity: 100%; | ||||||
|  |   } | ||||||
|  |   75% { | ||||||
|  |     opacity: 0; | ||||||
|  |   } | ||||||
|  |   100% { | ||||||
|  |     opacity: 100%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -18,6 +18,10 @@ import { DocumentDetailComponent } from '../document-detail/document-detail.comp | |||||||
| import { Meta } from '@angular/platform-browser' | import { Meta } from '@angular/platform-browser' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | ||||||
|  | import { | ||||||
|  |   RemoteVersionService, | ||||||
|  |   AppRemoteVersion, | ||||||
|  | } from 'src/app/services/rest/remote-version.service' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-app-frame', |   selector: 'app-app-frame', | ||||||
| @@ -32,10 +36,18 @@ export class AppFrameComponent { | |||||||
|     private searchService: SearchService, |     private searchService: SearchService, | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
|     private list: DocumentListViewService, |     private list: DocumentListViewService, | ||||||
|     private meta: Meta |     private meta: Meta, | ||||||
|   ) {} |     private remoteVersionService: RemoteVersionService | ||||||
|  |   ) { | ||||||
|  |     this.remoteVersionService | ||||||
|  |       .checkForUpdates() | ||||||
|  |       .subscribe((appRemoteVersion: AppRemoteVersion) => { | ||||||
|  |         this.appRemoteVersion = appRemoteVersion | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   versionString = `${environment.appTitle} ${environment.version}` |   versionString = `${environment.appTitle} ${environment.version}` | ||||||
|  |   appRemoteVersion | ||||||
|  |  | ||||||
|   isMenuCollapsed: boolean = true |   isMenuCollapsed: boolean = true | ||||||
|  |  | ||||||
| @@ -81,7 +93,10 @@ export class AppFrameComponent { | |||||||
|   search() { |   search() { | ||||||
|     this.closeMenu() |     this.closeMenu() | ||||||
|     this.list.quickFilter([ |     this.list.quickFilter([ | ||||||
|       { rule_type: FILTER_FULLTEXT_QUERY, value: this.searchField.value }, |       { | ||||||
|  |         rule_type: FILTER_FULLTEXT_QUERY, | ||||||
|  |         value: (this.searchField.value as string).trim(), | ||||||
|  |       }, | ||||||
|     ]) |     ]) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ | |||||||
|   filter: brightness(0.5); |   filter: brightness(0.5); | ||||||
|  |  | ||||||
|   &.active { |   &.active { | ||||||
|     background-color: var(--ngx-primary-lighten-30); |     background-color: var(--pngx-primary-lighten-30); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,2 +1,2 @@ | |||||||
| <span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> | <span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> | ||||||
| <a [routerLink]="[]" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> | <a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | a { | ||||||
|  |     cursor: pointer; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   *ngFor="let toast of toasts" |   *ngFor="let toast of toasts" | ||||||
|   [header]="toast.title" [autohide]="true" [delay]="toast.delay" |   [header]="toast.title" [autohide]="true" [delay]="toast.delay" | ||||||
|   [class]="toast.classname" |   [class]="toast.classname" | ||||||
|   (hide)="toastService.closeToast(toast)"> |   (hidden)="toastService.closeToast(toast)"> | ||||||
|   <p>{{toast.content}}</p> |   <p>{{toast.content}}</p> | ||||||
|   <p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> |   <p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> | ||||||
| </ngb-toast> | </ngb-toast> | ||||||
|   | |||||||
| @@ -5,3 +5,7 @@ | |||||||
|   margin: 0.5em; |   margin: 0.5em; | ||||||
|   z-index: 1200; |   z-index: 1200; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .toast:not(.show) { | ||||||
|  |   display: block; // this corrects an ng-bootstrap bug that prevented animations | ||||||
|  | } | ||||||
| @@ -33,3 +33,7 @@ form { | |||||||
|   mix-blend-mode: soft-light; |   mix-blend-mode: soft-light; | ||||||
|   pointer-events: none; |   pointer-events: none; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ::ng-deep .ngx-file-drop__drop-zone--over { | ||||||
|  |   background-color: var(--pngx-primary-faded) !important; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import { | |||||||
|   FileStatus, |   FileStatus, | ||||||
|   FileStatusPhase, |   FileStatusPhase, | ||||||
| } from 'src/app/services/consumer-status.service' | } from 'src/app/services/consumer-status.service' | ||||||
| import { DocumentService } from 'src/app/services/rest/document.service' | import { UploadDocumentsService } from 'src/app/services/upload-documents.service' | ||||||
|  |  | ||||||
| const MAX_ALERTS = 5 | const MAX_ALERTS = 5 | ||||||
|  |  | ||||||
| @@ -19,8 +19,8 @@ export class UploadFileWidgetComponent implements OnInit { | |||||||
|   alertsExpanded = false |   alertsExpanded = false | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     private documentService: DocumentService, |     private consumerStatusService: ConsumerStatusService, | ||||||
|     private consumerStatusService: ConsumerStatusService |     private uploadDocumentsService: UploadDocumentsService | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   getStatus() { |   getStatus() { | ||||||
| @@ -116,48 +116,6 @@ export class UploadFileWidgetComponent implements OnInit { | |||||||
|   public fileLeave(event) {} |   public fileLeave(event) {} | ||||||
|  |  | ||||||
|   public dropped(files: NgxFileDropEntry[]) { |   public dropped(files: NgxFileDropEntry[]) { | ||||||
|     for (const droppedFile of files) { |     this.uploadDocumentsService.uploadFiles(files) | ||||||
|       if (droppedFile.fileEntry.isFile) { |  | ||||||
|         const fileEntry = droppedFile.fileEntry as FileSystemFileEntry |  | ||||||
|         fileEntry.file((file: File) => { |  | ||||||
|           let formData = new FormData() |  | ||||||
|           formData.append('document', file, file.name) |  | ||||||
|           let status = this.consumerStatusService.newFileUpload(file.name) |  | ||||||
|  |  | ||||||
|           status.message = $localize`Connecting...` |  | ||||||
|  |  | ||||||
|           this.documentService.uploadDocument(formData).subscribe( |  | ||||||
|             (event) => { |  | ||||||
|               if (event.type == HttpEventType.UploadProgress) { |  | ||||||
|                 status.updateProgress( |  | ||||||
|                   FileStatusPhase.UPLOADING, |  | ||||||
|                   event.loaded, |  | ||||||
|                   event.total |  | ||||||
|                 ) |  | ||||||
|                 status.message = $localize`Uploading...` |  | ||||||
|               } else if (event.type == HttpEventType.Response) { |  | ||||||
|                 status.taskId = event.body['task_id'] |  | ||||||
|                 status.message = $localize`Upload complete, waiting...` |  | ||||||
|               } |  | ||||||
|             }, |  | ||||||
|             (error) => { |  | ||||||
|               switch (error.status) { |  | ||||||
|                 case 400: { |  | ||||||
|                   this.consumerStatusService.fail(status, error.error.document) |  | ||||||
|                   break |  | ||||||
|                 } |  | ||||||
|                 default: { |  | ||||||
|                   this.consumerStatusService.fail( |  | ||||||
|                     status, |  | ||||||
|                     $localize`HTTP error: ${error.status} ${error.statusText}` |  | ||||||
|                   ) |  | ||||||
|                   break |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           ) |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -135,20 +135,27 @@ | |||||||
|                 </li> |                 </li> | ||||||
|  |  | ||||||
|                 <li [ngbNavItem]="4" class="d-md-none"> |                 <li [ngbNavItem]="4" class="d-md-none"> | ||||||
|                   <a ngbNavLink>Preview</a> |                     <a ngbNavLink>Preview</a> | ||||||
|                   <ng-template ngbNavContent *ngIf="pdfPreview.offsetParent == undefined"> |                     <ng-template ngbNavContent *ngIf="pdfPreview.offsetParent == undefined"> | ||||||
|                     <ng-container *ngIf="getContentType() == 'application/pdf'"> |                         <div class="position-relative"> | ||||||
|                         <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> |                             <ng-container *ngIf="getContentType() == 'application/pdf'"> | ||||||
|                             <pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [render-text-mode]="2"></pdf-viewer> |                                 <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> | ||||||
|  |                                     <pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer> | ||||||
|  |                                 </div> | ||||||
|  |                                 <ng-template #nativePdfViewer> | ||||||
|  |                                     <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> | ||||||
|  |                                 </ng-template> | ||||||
|  |                             </ng-container> | ||||||
|  |                             <ng-container *ngIf="getContentType() == 'text/plain'"> | ||||||
|  |                                 <object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object> | ||||||
|  |                             </ng-container> | ||||||
|  |                             <div *ngIf="requiresPassword" class="password-prompt"> | ||||||
|  |                                 <form> | ||||||
|  |                                     <input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" /> | ||||||
|  |                                 </form> | ||||||
|  |                             </div> | ||||||
|                         </div> |                         </div> | ||||||
|                         <ng-template #nativePdfViewer> |                     </ng-template> | ||||||
|                             <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> |  | ||||||
|                         </ng-template> |  | ||||||
|                     </ng-container> |  | ||||||
|                     <ng-container *ngIf="getContentType() == 'text/plain'"> |  | ||||||
|                         <object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object> |  | ||||||
|                     </ng-container> |  | ||||||
|                   </ng-template> |  | ||||||
|                 </li> |                 </li> | ||||||
|             </ul> |             </ul> | ||||||
|  |  | ||||||
| @@ -160,10 +167,10 @@ | |||||||
|         </form> |         </form> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="col-md-6 col-xl-8 mb-3 d-none d-md-block" #pdfPreview> |     <div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview> | ||||||
|         <ng-container *ngIf="getContentType() == 'application/pdf'"> |         <ng-container *ngIf="getContentType() == 'application/pdf'"> | ||||||
|             <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> |             <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer"> | ||||||
|                 <pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer> |                 <pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer> | ||||||
|             </div> |             </div> | ||||||
|             <ng-template #nativePdfViewer> |             <ng-template #nativePdfViewer> | ||||||
|                 <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> |                 <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> | ||||||
| @@ -172,5 +179,11 @@ | |||||||
|         <ng-container *ngIf="getContentType() == 'text/plain'"> |         <ng-container *ngIf="getContentType() == 'text/plain'"> | ||||||
|             <object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object> |             <object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|  |         <div *ngIf="requiresPassword" class="password-prompt"> | ||||||
|  |             <form> | ||||||
|  |                 <input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" /> | ||||||
|  |             </form> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -17,3 +17,10 @@ | |||||||
|   --page-margin: 1px 0 -8px; |   --page-margin: 1px 0 -8px; | ||||||
|   width: 100% !important; |   width: 100% !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .password-prompt { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 30%; | ||||||
|  |   left: 30%; | ||||||
|  |   right: 30%; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,10 +1,4 @@ | |||||||
| import { | import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core' | ||||||
|   Component, |  | ||||||
|   OnInit, |  | ||||||
|   OnDestroy, |  | ||||||
|   ViewChild, |  | ||||||
|   ElementRef, |  | ||||||
| } from '@angular/core' |  | ||||||
| import { FormControl, FormGroup } from '@angular/forms' | import { FormControl, FormGroup } from '@angular/forms' | ||||||
| import { ActivatedRoute, Router } from '@angular/router' | import { ActivatedRoute, Router } from '@angular/router' | ||||||
| import { NgbModal, NgbNav } from '@ng-bootstrap/ng-bootstrap' | import { NgbModal, NgbNav } from '@ng-bootstrap/ng-bootstrap' | ||||||
| @@ -90,6 +84,11 @@ export class DocumentDetailComponent | |||||||
|   isDirty$: Observable<boolean> |   isDirty$: Observable<boolean> | ||||||
|   unsubscribeNotifier: Subject<any> = new Subject() |   unsubscribeNotifier: Subject<any> = new Subject() | ||||||
|  |  | ||||||
|  |   requiresPassword: boolean = false | ||||||
|  |   password: string | ||||||
|  |  | ||||||
|  |   ogDate: Date | ||||||
|  |  | ||||||
|   @ViewChild('nav') nav: NgbNav |   @ViewChild('nav') nav: NgbNav | ||||||
|   @ViewChild('pdfPreview') set pdfPreview(element) { |   @ViewChild('pdfPreview') set pdfPreview(element) { | ||||||
|     // this gets called when compontent added or removed from DOM |     // this gets called when compontent added or removed from DOM | ||||||
| @@ -145,7 +144,21 @@ export class DocumentDetailComponent | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.documentForm.valueChanges |     this.documentForm.valueChanges | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe((wow) => { |       .subscribe((changes) => { | ||||||
|  |         if (this.ogDate) { | ||||||
|  |           let newDate = new Date(changes['created']) | ||||||
|  |           newDate.setHours( | ||||||
|  |             this.ogDate.getHours(), | ||||||
|  |             this.ogDate.getMinutes(), | ||||||
|  |             this.ogDate.getSeconds(), | ||||||
|  |             this.ogDate.getMilliseconds() | ||||||
|  |           ) | ||||||
|  |           this.documentForm.patchValue( | ||||||
|  |             { created: this.formatDate(newDate) }, | ||||||
|  |             { emitEvent: false } | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|         Object.assign(this.document, this.documentForm.value) |         Object.assign(this.document, this.documentForm.value) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
| @@ -186,17 +199,25 @@ export class DocumentDetailComponent | |||||||
|             this.updateComponent(doc) |             this.updateComponent(doc) | ||||||
|           } |           } | ||||||
|  |  | ||||||
|  |           this.ogDate = new Date(doc.created) | ||||||
|  |  | ||||||
|           // Initialize dirtyCheck |           // Initialize dirtyCheck | ||||||
|           this.store = new BehaviorSubject({ |           this.store = new BehaviorSubject({ | ||||||
|             title: doc.title, |             title: doc.title, | ||||||
|             content: doc.content, |             content: doc.content, | ||||||
|             created: doc.created, |             created: this.formatDate(this.ogDate), | ||||||
|             correspondent: doc.correspondent, |             correspondent: doc.correspondent, | ||||||
|             document_type: doc.document_type, |             document_type: doc.document_type, | ||||||
|             archive_serial_number: doc.archive_serial_number, |             archive_serial_number: doc.archive_serial_number, | ||||||
|             tags: [...doc.tags], |             tags: [...doc.tags], | ||||||
|           }) |           }) | ||||||
|  |  | ||||||
|  |           // ensure we're always starting with 24-char ISO8601 string | ||||||
|  |           this.documentForm.patchValue( | ||||||
|  |             { created: this.formatDate(this.ogDate) }, | ||||||
|  |             { emitEvent: false } | ||||||
|  |           ) | ||||||
|  |  | ||||||
|           this.isDirty$ = dirtyCheck( |           this.isDirty$ = dirtyCheck( | ||||||
|             this.documentForm, |             this.documentForm, | ||||||
|             this.store.asObservable() |             this.store.asObservable() | ||||||
| @@ -450,5 +471,22 @@ export class DocumentDetailComponent | |||||||
|  |  | ||||||
|   pdfPreviewLoaded(pdf: PDFDocumentProxy) { |   pdfPreviewLoaded(pdf: PDFDocumentProxy) { | ||||||
|     this.previewNumPages = pdf.numPages |     this.previewNumPages = pdf.numPages | ||||||
|  |     if (this.password) this.requiresPassword = false | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onError(event) { | ||||||
|  |     if (event.name == 'PasswordException') { | ||||||
|  |       this.requiresPassword = true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onPasswordKeyUp(event: KeyboardEvent) { | ||||||
|  |     if ('Enter' == event.key) { | ||||||
|  |       this.password = (event.target as HTMLInputElement).value | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   formatDate(date: Date): string { | ||||||
|  |     return date.toISOString().split('.')[0] + 'Z' | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -57,13 +57,18 @@ | |||||||
|   </div> |   </div> | ||||||
|   <div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> |   <div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> | ||||||
|     <div class="btn-group btn-group-sm me-2"> |     <div class="btn-group btn-group-sm me-2"> | ||||||
|       <button type="button" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()"> |       <button type="button" [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()"> | ||||||
|         <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> |         <svg *ngIf="!awaitingDownload" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||||
|           <use xlink:href="assets/bootstrap-icons.svg#download" /> |           <use xlink:href="assets/bootstrap-icons.svg#download" /> | ||||||
|         </svg> <ng-container i18n>Download</ng-container> |         </svg> | ||||||
|  |         <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||||
|  |           <span class="visually-hidden">Preparing download...</span> | ||||||
|  |         </div> | ||||||
|  |           | ||||||
|  |         <ng-container i18n>Download</ng-container> | ||||||
|       </button> |       </button> | ||||||
|       <div class="btn-group" ngbDropdown role="group" aria-label="Button group with nested dropdown"> |       <div class="btn-group" ngbDropdown role="group" aria-label="Button group with nested dropdown"> | ||||||
|         <button class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button> |         <button [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button> | ||||||
|         <div class="dropdown-menu shadow" ngbDropdownMenu> |         <div class="dropdown-menu shadow" ngbDropdownMenu> | ||||||
|           <button ngbDropdownItem i18n (click)="downloadSelected('originals')">Download originals</button> |           <button ngbDropdownItem i18n (click)="downloadSelected('originals')">Download originals</button> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ export class BulkEditorComponent { | |||||||
|   tagSelectionModel = new FilterableDropdownSelectionModel() |   tagSelectionModel = new FilterableDropdownSelectionModel() | ||||||
|   correspondentSelectionModel = new FilterableDropdownSelectionModel() |   correspondentSelectionModel = new FilterableDropdownSelectionModel() | ||||||
|   documentTypeSelectionModel = new FilterableDropdownSelectionModel() |   documentTypeSelectionModel = new FilterableDropdownSelectionModel() | ||||||
|  |   awaitingDownload: boolean | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     private documentTypeService: DocumentTypeService, |     private documentTypeService: DocumentTypeService, | ||||||
| @@ -317,10 +318,12 @@ export class BulkEditorComponent { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   downloadSelected(content = 'archive') { |   downloadSelected(content = 'archive') { | ||||||
|  |     this.awaitingDownload = true | ||||||
|     this.documentService |     this.documentService | ||||||
|       .bulkDownload(Array.from(this.list.selected), content) |       .bulkDownload(Array.from(this.list.selected), content) | ||||||
|       .subscribe((result: any) => { |       .subscribe((result: any) => { | ||||||
|         saveAs(result, 'documents.zip') |         saveAs(result, 'documents.zip') | ||||||
|  |         this.awaitingDownload = false | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
|         <div class="d-flex justify-content-between align-items-center"> |         <div class="d-flex justify-content-between align-items-center"> | ||||||
|           <h5 class="card-title"> |           <h5 class="card-title"> | ||||||
|             <ng-container *ngIf="document.correspondent"> |             <ng-container *ngIf="document.correspondent"> | ||||||
|               <a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a> |               <a *ngIf="clickCorrespondent.observers.length ; else nolink" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a> | ||||||
|               <ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: |               <ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: | ||||||
|             </ng-container> |             </ng-container> | ||||||
|             {{document.title | documentTitle}} |             {{document.title | documentTitle}} | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .doc-img-background-selected { | .doc-img-background-selected { | ||||||
|   background-color: var(--ngx-primary-faded); |   background-color: var(--pngx-primary-faded); | ||||||
| } | } | ||||||
|  |  | ||||||
| .card-info { | .card-info { | ||||||
| @@ -90,3 +90,7 @@ span ::ng-deep .match { | |||||||
|   color: black; |   color: black; | ||||||
|   background-color: rgb(255, 211, 66); |   background-color: rgb(255, 211, 66); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | a { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ | |||||||
|     <div class="card-body p-2"> |     <div class="card-body p-2"> | ||||||
|       <p class="card-text"> |       <p class="card-text"> | ||||||
|         <ng-container *ngIf="document.correspondent"> |         <ng-container *ngIf="document.correspondent"> | ||||||
|           <a [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>: |           <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>: | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         {{document.title | documentTitle}} |         {{document.title | documentTitle}} | ||||||
|       </p> |       </p> | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .doc-img-background-selected { | .doc-img-background-selected { | ||||||
|   background-color: var(--ngx-primary-faded); |   background-color: var(--pngx-primary-faded); | ||||||
| } | } | ||||||
|  |  | ||||||
| .card-info { | .card-info { | ||||||
| @@ -76,3 +76,7 @@ | |||||||
|   text-align: left !important; |   text-align: left !important; | ||||||
|   font-size: 90%; |   font-size: 90%; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | a { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ | |||||||
|  |  | ||||||
| </app-page-header> | </app-page-header> | ||||||
|  |  | ||||||
| <div class="sticky-top py-2 mt-n2 mt-sm-n3 py-sm-4 bg-body mx-n3 px-3"> | <div class="row sticky-top pt-4 pb-2 pb-lg-4 bg-body"> | ||||||
|   <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" #filterEditor></app-filter-editor> |   <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" #filterEditor></app-filter-editor> | ||||||
|   <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor> |   <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor> | ||||||
| </div> | </div> | ||||||
| @@ -100,7 +100,7 @@ | |||||||
| <ng-container *ngTemplateOutlet="pagination"></ng-container> | <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||||
|  |  | ||||||
| <ng-container *ngIf="list.error ; else documentListNoError"> | <ng-container *ngIf="list.error ; else documentListNoError"> | ||||||
|   <div class="alert alert-danger" role="alert">Error while loading documents: {{list.error}}</div> |   <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> | ||||||
| </ng-container> | </ng-container> | ||||||
|  |  | ||||||
| <ng-template #documentListNoError> | <ng-template #documentListNoError> | ||||||
| @@ -163,7 +163,7 @@ | |||||||
|         </td> |         </td> | ||||||
|         <td class="d-none d-md-table-cell"> |         <td class="d-none d-md-table-cell"> | ||||||
|           <ng-container *ngIf="d.correspondent"> |           <ng-container *ngIf="d.correspondent"> | ||||||
|             <a [routerLink]="[]" (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> |             <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> | ||||||
|           </ng-container> |           </ng-container> | ||||||
|         </td> |         </td> | ||||||
|         <td> |         <td> | ||||||
| @@ -172,7 +172,7 @@ | |||||||
|         </td> |         </td> | ||||||
|         <td class="d-none d-xl-table-cell"> |         <td class="d-none d-xl-table-cell"> | ||||||
|           <ng-container *ngIf="d.document_type"> |           <ng-container *ngIf="d.document_type"> | ||||||
|             <a [routerLink]="[]" (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> |             <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> | ||||||
|           </ng-container> |           </ng-container> | ||||||
|         </td> |         </td> | ||||||
|         <td> |         <td> | ||||||
| @@ -185,7 +185,7 @@ | |||||||
|     </tbody> |     </tbody> | ||||||
|   </table> |   </table> | ||||||
|  |  | ||||||
|   <div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> |   <div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> | ||||||
|     <app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small> |     <app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small> | ||||||
|   </div> |   </div> | ||||||
|   <div *ngIf="list.documents?.length > 15" class="mt-3"> |   <div *ngIf="list.documents?.length > 15" class="mt-3"> | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| @import "/src/theme"; | @import "/src/theme"; | ||||||
|  |  | ||||||
|  | ::ng-deep app-document-list app-page-header > div.mb-3 { | ||||||
|  |   margin-bottom: 0 !important; | ||||||
|  | } | ||||||
|  |  | ||||||
| tr { | tr { | ||||||
|   user-select: none; |   user-select: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| .table-row-selected { | .table-row-selected { | ||||||
|   background-color: var(--ngx-primary-faded); |   background-color: var(--pngx-primary-faded); | ||||||
| } | } | ||||||
|  |  | ||||||
| $paperless-card-breakpoints: ( | $paperless-card-breakpoints: ( | ||||||
| @@ -53,3 +57,7 @@ $paperless-card-breakpoints: ( | |||||||
|     margin-left: 0; |     margin-left: 0; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | a { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { | import { | ||||||
|  |   AfterViewInit, | ||||||
|   Component, |   Component, | ||||||
|   OnDestroy, |   OnDestroy, | ||||||
|   OnInit, |   OnInit, | ||||||
| @@ -8,9 +9,20 @@ import { | |||||||
| } from '@angular/core' | } from '@angular/core' | ||||||
| import { ActivatedRoute, Router } from '@angular/router' | import { ActivatedRoute, Router } from '@angular/router' | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { Subscription } from 'rxjs' | import { | ||||||
|  |   filter, | ||||||
|  |   first, | ||||||
|  |   map, | ||||||
|  |   Subject, | ||||||
|  |   Subscription, | ||||||
|  |   switchMap, | ||||||
|  |   takeUntil, | ||||||
|  | } from 'rxjs' | ||||||
| import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule' | import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule' | ||||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | import { | ||||||
|  |   FILTER_FULLTEXT_MORELIKE, | ||||||
|  |   FILTER_RULE_TYPES, | ||||||
|  | } from 'src/app/data/filter-rule-type' | ||||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||||
| import { | import { | ||||||
| @@ -20,6 +32,7 @@ import { | |||||||
| import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { | import { | ||||||
|  |   DocumentService, | ||||||
|   DOCUMENT_SORT_FIELDS, |   DOCUMENT_SORT_FIELDS, | ||||||
|   DOCUMENT_SORT_FIELDS_FULLTEXT, |   DOCUMENT_SORT_FIELDS_FULLTEXT, | ||||||
| } from 'src/app/services/rest/document.service' | } from 'src/app/services/rest/document.service' | ||||||
| @@ -33,9 +46,10 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi | |||||||
|   templateUrl: './document-list.component.html', |   templateUrl: './document-list.component.html', | ||||||
|   styleUrls: ['./document-list.component.scss'], |   styleUrls: ['./document-list.component.scss'], | ||||||
| }) | }) | ||||||
| export class DocumentListComponent implements OnInit, OnDestroy { | export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | ||||||
|   constructor( |   constructor( | ||||||
|     public list: DocumentListViewService, |     public list: DocumentListViewService, | ||||||
|  |     private documentService: DocumentService, | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
|     public route: ActivatedRoute, |     public route: ActivatedRoute, | ||||||
|     private router: Router, |     private router: Router, | ||||||
| @@ -53,7 +67,7 @@ export class DocumentListComponent implements OnInit, OnDestroy { | |||||||
|  |  | ||||||
|   unmodifiedFilterRules: FilterRule[] = [] |   unmodifiedFilterRules: FilterRule[] = [] | ||||||
|  |  | ||||||
|   private consumptionFinishedSubscription: Subscription |   private unsubscribeNotifier: Subject<any> = new Subject() | ||||||
|  |  | ||||||
|   get isFiltered() { |   get isFiltered() { | ||||||
|     return this.list.filterRules?.length > 0 |     return this.list.filterRules?.length > 0 | ||||||
| @@ -85,34 +99,97 @@ export class DocumentListComponent implements OnInit, OnDestroy { | |||||||
|     if (localStorage.getItem('document-list:displayMode') != null) { |     if (localStorage.getItem('document-list:displayMode') != null) { | ||||||
|       this.displayMode = localStorage.getItem('document-list:displayMode') |       this.displayMode = localStorage.getItem('document-list:displayMode') | ||||||
|     } |     } | ||||||
|     this.consumptionFinishedSubscription = this.consumerStatusService |  | ||||||
|  |     this.consumerStatusService | ||||||
|       .onDocumentConsumptionFinished() |       .onDocumentConsumptionFinished() | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe(() => { |       .subscribe(() => { | ||||||
|         this.list.reload() |         this.list.reload() | ||||||
|       }) |       }) | ||||||
|     this.route.paramMap.subscribe((params) => { |  | ||||||
|       if (params.has('id')) { |     this.route.paramMap | ||||||
|         this.savedViewService.getCached(+params.get('id')).subscribe((view) => { |       .pipe( | ||||||
|           if (!view) { |         filter((params) => params.has('id')), // only on saved view | ||||||
|             this.router.navigate(['404']) |         switchMap((params) => { | ||||||
|             return |           return this.savedViewService | ||||||
|           } |             .getCached(+params.get('id')) | ||||||
|           this.list.activateSavedView(view) |             .pipe(map((view) => ({ params, view }))) | ||||||
|           this.list.reload() |  | ||||||
|           this.unmodifiedFilterRules = view.filter_rules |  | ||||||
|         }) |         }) | ||||||
|       } else { |       ) | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|  |       .subscribe(({ view, params }) => { | ||||||
|  |         if (!view) { | ||||||
|  |           this.router.navigate(['404']) | ||||||
|  |           return | ||||||
|  |         } | ||||||
|  |         this.list.activateSavedView(view) | ||||||
|  |         this.list.reload() | ||||||
|  |         this.unmodifiedFilterRules = view.filter_rules | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |     const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map( | ||||||
|  |       (rt) => rt.filtervar | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     this.route.queryParamMap | ||||||
|  |       .pipe( | ||||||
|  |         filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on saved view | ||||||
|  |         takeUntil(this.unsubscribeNotifier) | ||||||
|  |       ) | ||||||
|  |       .subscribe((queryParams) => { | ||||||
|  |         // transform query params to filter rules | ||||||
|  |         let filterRulesFromQueryParams: FilterRule[] = [] | ||||||
|  |         allFilterRuleQueryParams | ||||||
|  |           .filter((frqp) => queryParams.has(frqp)) | ||||||
|  |           .forEach((filterQueryParamName) => { | ||||||
|  |             const filterQueryParamValues: string[] = queryParams | ||||||
|  |               .get(filterQueryParamName) | ||||||
|  |               .split(',') | ||||||
|  |  | ||||||
|  |             filterRulesFromQueryParams = filterRulesFromQueryParams.concat( | ||||||
|  |               // map all values to filter rules | ||||||
|  |               filterQueryParamValues.map((val) => { | ||||||
|  |                 return { | ||||||
|  |                   rule_type: FILTER_RULE_TYPES.find( | ||||||
|  |                     (rt) => rt.filtervar == filterQueryParamName | ||||||
|  |                   ).id, | ||||||
|  |                   value: val, | ||||||
|  |                 } | ||||||
|  |               }) | ||||||
|  |             ) | ||||||
|  |           }) | ||||||
|  |  | ||||||
|         this.list.activateSavedView(null) |         this.list.activateSavedView(null) | ||||||
|  |         this.list.filterRules = filterRulesFromQueryParams | ||||||
|         this.list.reload() |         this.list.reload() | ||||||
|         this.unmodifiedFilterRules = [] |         this.unmodifiedFilterRules = [] | ||||||
|       } |       }) | ||||||
|     }) |   } | ||||||
|  |  | ||||||
|  |   ngAfterViewInit(): void { | ||||||
|  |     this.filterEditor.filterRulesChange | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|  |       .subscribe({ | ||||||
|  |         next: (filterRules) => { | ||||||
|  |           const params = | ||||||
|  |             this.documentService.filterRulesToQueryParams(filterRules) | ||||||
|  |  | ||||||
|  |           // if we were on a saved view we navigate 'away' to /documents | ||||||
|  |           let base = [] | ||||||
|  |           if (this.route.snapshot.paramMap.has('id')) base = ['/documents'] | ||||||
|  |  | ||||||
|  |           this.router.navigate(base, { | ||||||
|  |             relativeTo: this.route, | ||||||
|  |             queryParams: params, | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngOnDestroy() { |   ngOnDestroy() { | ||||||
|     if (this.consumptionFinishedSubscription) { |     // unsubscribes all | ||||||
|       this.consumptionFinishedSubscription.unsubscribe() |     this.unsubscribeNotifier.next(this) | ||||||
|     } |     this.unsubscribeNotifier.complete() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   loadViewConfig(view: PaperlessSavedView) { |   loadViewConfig(view: PaperlessSavedView) { | ||||||
| @@ -128,12 +205,15 @@ export class DocumentListComponent implements OnInit, OnDestroy { | |||||||
|         sort_field: this.list.sortField, |         sort_field: this.list.sortField, | ||||||
|         sort_reverse: this.list.sortReverse, |         sort_reverse: this.list.sortReverse, | ||||||
|       } |       } | ||||||
|       this.savedViewService.patch(savedView).subscribe((result) => { |       this.savedViewService | ||||||
|         this.toastService.showInfo( |         .patch(savedView) | ||||||
|           $localize`View "${this.list.activeSavedViewTitle}" saved successfully.` |         .pipe(first()) | ||||||
|         ) |         .subscribe((result) => { | ||||||
|         this.unmodifiedFilterRules = this.list.filterRules |           this.toastService.showInfo( | ||||||
|       }) |             $localize`View "${this.list.activeSavedViewTitle}" saved successfully.` | ||||||
|  |           ) | ||||||
|  |           this.unmodifiedFilterRules = this.list.filterRules | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -142,7 +222,7 @@ export class DocumentListComponent implements OnInit, OnDestroy { | |||||||
|       backdrop: 'static', |       backdrop: 'static', | ||||||
|     }) |     }) | ||||||
|     modal.componentInstance.defaultName = this.filterEditor.generateFilterName() |     modal.componentInstance.defaultName = this.filterEditor.generateFilterName() | ||||||
|     modal.componentInstance.saveClicked.subscribe((formValue) => { |     modal.componentInstance.saveClicked.pipe(first()).subscribe((formValue) => { | ||||||
|       modal.componentInstance.buttonsEnabled = false |       modal.componentInstance.buttonsEnabled = false | ||||||
|       let savedView: PaperlessSavedView = { |       let savedView: PaperlessSavedView = { | ||||||
|         name: formValue.name, |         name: formValue.name, | ||||||
| @@ -153,18 +233,25 @@ export class DocumentListComponent implements OnInit, OnDestroy { | |||||||
|         sort_field: this.list.sortField, |         sort_field: this.list.sortField, | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.savedViewService.create(savedView).subscribe( |       this.savedViewService | ||||||
|         () => { |         .create(savedView) | ||||||
|           modal.close() |         .pipe(first()) | ||||||
|           this.toastService.showInfo( |         .subscribe({ | ||||||
|             $localize`View "${savedView.name}" created successfully.` |           next: () => { | ||||||
|           ) |             modal.close() | ||||||
|         }, |             this.toastService.showInfo( | ||||||
|         (error) => { |               $localize`View "${savedView.name}" created successfully.` | ||||||
|           modal.componentInstance.error = error.error |             ) | ||||||
|           modal.componentInstance.buttonsEnabled = true |           }, | ||||||
|         } |           error: (httpError) => { | ||||||
|       ) |             let error = httpError.error | ||||||
|  |             if (error.filter_rules) { | ||||||
|  |               error.filter_rules = error.filter_rules.map((r) => r.value) | ||||||
|  |             } | ||||||
|  |             modal.componentInstance.error = error | ||||||
|  |             modal.componentInstance.buttonsEnabled = true | ||||||
|  |           }, | ||||||
|  |         }) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ | |||||||
|      </div> |      </div> | ||||||
|    </div> |    </div> | ||||||
|    <div class="w-100 d-xl-none"></div> |    <div class="w-100 d-xl-none"></div> | ||||||
|    <div class="col col-xl-auto mb-2 mb-xl-0"> |    <div class="col col-xl-auto"> | ||||||
|      <button class="btn btn-link btn-sm px-0 mx-0 ms-xl-n3" [disabled]="!rulesModified" (click)="resetSelected()"> |      <button class="btn btn-link btn-sm px-0 mx-0 ms-xl-n3" [disabled]="!rulesModified" (click)="resetSelected()"> | ||||||
|        <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> |        <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
|          <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> |          <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> | ||||||
|   | |||||||
| @@ -8,6 +8,11 @@ | |||||||
|     <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> |     <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||||
|     <app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check> |     <app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check> | ||||||
|     <app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check> |     <app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check> | ||||||
|  |     <div *ngIf="error?.filter_rules" class="alert alert-danger" role="alert"> | ||||||
|  |       <h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6> | ||||||
|  |       <ng-container i18n>The error returned was</ng-container>:<br/> | ||||||
|  |       {{ error.filter_rules }} | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="modal-footer"> |   <div class="modal-footer"> | ||||||
|     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button> |     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button> | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import { | |||||||
| import { PaperlessDocument } from '../data/paperless-document' | import { PaperlessDocument } from '../data/paperless-document' | ||||||
| import { PaperlessSavedView } from '../data/paperless-saved-view' | import { PaperlessSavedView } from '../data/paperless-saved-view' | ||||||
| import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys' | import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys' | ||||||
| import { DocumentService } from './rest/document.service' | import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service' | ||||||
| import { SettingsService, SETTINGS_KEYS } from './settings.service' | import { SettingsService, SETTINGS_KEYS } from './settings.service' | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -143,8 +143,8 @@ export class DocumentListViewService { | |||||||
|         activeListViewState.sortReverse, |         activeListViewState.sortReverse, | ||||||
|         activeListViewState.filterRules |         activeListViewState.filterRules | ||||||
|       ) |       ) | ||||||
|       .subscribe( |       .subscribe({ | ||||||
|         (result) => { |         next: (result) => { | ||||||
|           this.isReloading = false |           this.isReloading = false | ||||||
|           activeListViewState.collectionSize = result.count |           activeListViewState.collectionSize = result.count | ||||||
|           activeListViewState.documents = result.results |           activeListViewState.documents = result.results | ||||||
| @@ -153,17 +153,34 @@ export class DocumentListViewService { | |||||||
|           } |           } | ||||||
|           this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null |           this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null | ||||||
|         }, |         }, | ||||||
|         (error) => { |         error: (error) => { | ||||||
|           this.isReloading = false |           this.isReloading = false | ||||||
|           if (activeListViewState.currentPage != 1 && error.status == 404) { |           if (activeListViewState.currentPage != 1 && error.status == 404) { | ||||||
|             // this happens when applying a filter: the current page might not be available anymore due to the reduced result set. |             // this happens when applying a filter: the current page might not be available anymore due to the reduced result set. | ||||||
|             activeListViewState.currentPage = 1 |             activeListViewState.currentPage = 1 | ||||||
|             this.reload() |             this.reload() | ||||||
|           } else { |           } else { | ||||||
|             this.error = error.error |             let errorMessage | ||||||
|  |             if ( | ||||||
|  |               typeof error.error !== 'string' && | ||||||
|  |               Object.keys(error.error).length > 0 | ||||||
|  |             ) { | ||||||
|  |               // e.g. { archive_serial_number: Array<string> } | ||||||
|  |               errorMessage = Object.keys(error.error) | ||||||
|  |                 .map((fieldName) => { | ||||||
|  |                   const fieldError: Array<string> = error.error[fieldName] | ||||||
|  |                   return `${ | ||||||
|  |                     DOCUMENT_SORT_FIELDS.find((f) => f.field == fieldName)?.name | ||||||
|  |                   }: ${fieldError[0]}` | ||||||
|  |                 }) | ||||||
|  |                 .join(', ') | ||||||
|  |             } else { | ||||||
|  |               errorMessage = error.error | ||||||
|  |             } | ||||||
|  |             this.error = errorMessage | ||||||
|           } |           } | ||||||
|         } |         }, | ||||||
|       ) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   set filterRules(filterRules: FilterRule[]) { |   set filterRules(filterRules: FilterRule[]) { | ||||||
| @@ -249,20 +266,11 @@ export class DocumentListViewService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   quickFilter(filterRules: FilterRule[]) { |   quickFilter(filterRules: FilterRule[]) { | ||||||
|     this._activeSavedViewId = null |     const params = this.documentService.filterRulesToQueryParams(filterRules) | ||||||
|     this.activeListViewState.filterRules = filterRules |     this.router.navigate(['/documents'], { | ||||||
|     this.activeListViewState.currentPage = 1 |       relativeTo: this.route, | ||||||
|     if (isFullTextFilterRule(filterRules)) { |       queryParams: params, | ||||||
|       this.activeListViewState.sortField = 'score' |     }) | ||||||
|       this.activeListViewState.sortReverse = false |  | ||||||
|     } |  | ||||||
|     this.reduceSelectionToFilter() |  | ||||||
|     this.saveDocumentListView() |  | ||||||
|     if (this.router.url == '/documents') { |  | ||||||
|       this.reload() |  | ||||||
|     } else { |  | ||||||
|       this.router.navigate(['documents']) |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getLastPage(): number { |   getLastPage(): number { | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | |||||||
|     super(http, 'documents') |     super(http, 'documents') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private filterRulesToQueryParams(filterRules: FilterRule[]) { |   public filterRulesToQueryParams(filterRules: FilterRule[]): Object { | ||||||
|     if (filterRules) { |     if (filterRules) { | ||||||
|       let params = {} |       let params = {} | ||||||
|       for (let rule of filterRules) { |       for (let rule of filterRules) { | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src-ui/src/app/services/rest/remote-version.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | import { HttpClient } from '@angular/common/http' | ||||||
|  | import { Injectable } from '@angular/core' | ||||||
|  | import { map, Observable } from 'rxjs' | ||||||
|  | import { environment } from 'src/environments/environment' | ||||||
|  |  | ||||||
|  | export interface AppRemoteVersion { | ||||||
|  |   version: string | ||||||
|  |   update_available: boolean | ||||||
|  |   feature_is_set: boolean | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root', | ||||||
|  | }) | ||||||
|  | export class RemoteVersionService { | ||||||
|  |   constructor(private http: HttpClient) {} | ||||||
|  |  | ||||||
|  |   public checkForUpdates(): Observable<AppRemoteVersion> { | ||||||
|  |     return this.http.get<AppRemoteVersion>( | ||||||
|  |       `${environment.apiBaseUrl}remote_version/` | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -168,6 +168,12 @@ export class SettingsService { | |||||||
|         englishName: 'English (US)', |         englishName: 'English (US)', | ||||||
|         dateInputFormat: 'mm/dd/yyyy', |         dateInputFormat: 'mm/dd/yyyy', | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         code: 'be-by', | ||||||
|  |         name: $localize`Belarusian`, | ||||||
|  |         englishName: 'Belarusian', | ||||||
|  |         dateInputFormat: 'dd.mm.yyyy', | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         code: 'cs-cz', |         code: 'cs-cz', | ||||||
|         name: $localize`Czech`, |         name: $localize`Czech`, | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								src-ui/src/app/services/upload-documents.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | |||||||
|  | import { Injectable } from '@angular/core' | ||||||
|  | import { HttpEventType } from '@angular/common/http' | ||||||
|  | import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop' | ||||||
|  | import { | ||||||
|  |   ConsumerStatusService, | ||||||
|  |   FileStatusPhase, | ||||||
|  | } from './consumer-status.service' | ||||||
|  | import { DocumentService } from './rest/document.service' | ||||||
|  | import { Subscription } from 'rxjs' | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root', | ||||||
|  | }) | ||||||
|  | export class UploadDocumentsService { | ||||||
|  |   private uploadSubscriptions: Array<Subscription> = [] | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     private documentService: DocumentService, | ||||||
|  |     private consumerStatusService: ConsumerStatusService | ||||||
|  |   ) {} | ||||||
|  |  | ||||||
|  |   uploadFiles(files: NgxFileDropEntry[]) { | ||||||
|  |     for (const droppedFile of files) { | ||||||
|  |       if (droppedFile.fileEntry.isFile) { | ||||||
|  |         const fileEntry = droppedFile.fileEntry as FileSystemFileEntry | ||||||
|  |         fileEntry.file((file: File) => { | ||||||
|  |           let formData = new FormData() | ||||||
|  |           formData.append('document', file, file.name) | ||||||
|  |           let status = this.consumerStatusService.newFileUpload(file.name) | ||||||
|  |  | ||||||
|  |           status.message = $localize`Connecting...` | ||||||
|  |  | ||||||
|  |           this.uploadSubscriptions[file.name] = this.documentService | ||||||
|  |             .uploadDocument(formData) | ||||||
|  |             .subscribe({ | ||||||
|  |               next: (event) => { | ||||||
|  |                 if (event.type == HttpEventType.UploadProgress) { | ||||||
|  |                   status.updateProgress( | ||||||
|  |                     FileStatusPhase.UPLOADING, | ||||||
|  |                     event.loaded, | ||||||
|  |                     event.total | ||||||
|  |                   ) | ||||||
|  |                   status.message = $localize`Uploading...` | ||||||
|  |                 } else if (event.type == HttpEventType.Response) { | ||||||
|  |                   status.taskId = event.body['task_id'] | ||||||
|  |                   status.message = $localize`Upload complete, waiting...` | ||||||
|  |                   this.uploadSubscriptions[file.name]?.complete() | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               error: (error) => { | ||||||
|  |                 switch (error.status) { | ||||||
|  |                   case 400: { | ||||||
|  |                     this.consumerStatusService.fail( | ||||||
|  |                       status, | ||||||
|  |                       error.error.document | ||||||
|  |                     ) | ||||||
|  |                     break | ||||||
|  |                   } | ||||||
|  |                   default: { | ||||||
|  |                     this.consumerStatusService.fail( | ||||||
|  |                       status, | ||||||
|  |                       $localize`HTTP error: ${error.status} ${error.statusText}` | ||||||
|  |                     ) | ||||||
|  |                     break | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |                 this.uploadSubscriptions[file.name]?.complete() | ||||||
|  |               }, | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 861 KiB | 
							
								
								
									
										2808
									
								
								src-ui/src/locale/messages.be_BY.xlf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -678,7 +678,7 @@ | |||||||
|           <context context-type="linenumber">117</context> |           <context context-type="linenumber">117</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note> |         <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note> | ||||||
|         <target state="needs-translation">, </target> |         <target state="translated">, </target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3852289441366561594" datatype="html" approved="yes"> |       <trans-unit id="3852289441366561594" datatype="html" approved="yes"> | ||||||
|         <source>Connecting...</source> |         <source>Connecting...</source> | ||||||
| @@ -1222,7 +1222,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">117</context> |           <context context-type="linenumber">117</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">"<x id="PH" equiv-text="items[0].name"/>"</target> |         <target state="translated">"<x id="PH" equiv-text="items[0].name"/>"</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8639884465898458690" datatype="html" approved="yes"> |       <trans-unit id="8639884465898458690" datatype="html" approved="yes"> | ||||||
|         <source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source> |         <source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source> | ||||||
|   | |||||||
							
								
								
									
										2808
									
								
								src-ui/src/locale/messages.fi_FI.xlf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -511,7 +511,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> |           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> | ||||||
|           <context context-type="linenumber">33</context> |           <context context-type="linenumber">33</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Bonjour <x id="PH" equiv-text="this.displayName"/>, bienvenue dans Paperless-ngx!</target> |         <target state="translated">Bonjour <x id="PH" equiv-text="this.displayName"/>, bienvenue dans Paperless-ngx !</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="795745990148149834" datatype="html"> |       <trans-unit id="795745990148149834" datatype="html"> | ||||||
|         <source>Welcome to Paperless-ngx!</source> |         <source>Welcome to Paperless-ngx!</source> | ||||||
| @@ -519,7 +519,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> |           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> | ||||||
|           <context context-type="linenumber">35</context> |           <context context-type="linenumber">35</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Bienvenue dans Paperless-ngx!</target> |         <target state="translated">Bienvenue dans Paperless-ngx !</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2946624699882754313" datatype="html" approved="yes"> |       <trans-unit id="2946624699882754313" datatype="html" approved="yes"> | ||||||
|         <source>Show all</source> |         <source>Show all</source> | ||||||
| @@ -1520,7 +1520,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> | ||||||
|           <context context-type="linenumber">87</context> |           <context context-type="linenumber">87</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Chargement ...</target> |         <target state="translated">Chargement…</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8786996283897742947" datatype="html" approved="yes"> |       <trans-unit id="8786996283897742947" datatype="html" approved="yes"> | ||||||
|         <source>{VAR_PLURAL, plural, =1 {Selected <x id="INTERPOLATION"/> of one document} other {Selected <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> documents}}</source> |         <source>{VAR_PLURAL, plural, =1 {Selected <x id="INTERPOLATION"/> of one document} other {Selected <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> documents}}</source> | ||||||
|   | |||||||
| @@ -433,7 +433,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> |           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> | ||||||
|           <context context-type="linenumber">45</context> |           <context context-type="linenumber">45</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Click again to exclude items.</target> |         <target state="translated">Нажмите снова, чтобы исключить элементы.</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7593728289020204896" datatype="html" approved="yes"> |       <trans-unit id="7593728289020204896" datatype="html" approved="yes"> | ||||||
|         <source>Not assigned</source> |         <source>Not assigned</source> | ||||||
| @@ -505,13 +505,13 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="final">Пожалуйста, выберите объект</target> |         <target state="final">Пожалуйста, выберите объект</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5412339817978503936" datatype="html"> |       <trans-unit id="5412339817978503936" datatype="html" approved="yes"> | ||||||
|         <source>Hello <x id="PH" equiv-text="this.displayName"/>, welcome to Paperless-ngx!</source> |         <source>Hello <x id="PH" equiv-text="this.displayName"/>, welcome to Paperless-ngx!</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> |           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> | ||||||
|           <context context-type="linenumber">33</context> |           <context context-type="linenumber">33</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Привет, <x id="PH" equiv-text="this.displayName"/>, добро пожаловать в Paperless-ngx!</target> |         <target state="final">Привет, <x id="PH" equiv-text="this.displayName"/>, добро пожаловать в Paperless-ngx!</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="795745990148149834" datatype="html"> |       <trans-unit id="795745990148149834" datatype="html"> | ||||||
|         <source>Welcome to Paperless-ngx!</source> |         <source>Welcome to Paperless-ngx!</source> | ||||||
| @@ -667,7 +667,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="final">Добавлено: <x id="PH" equiv-text="countSuccess"/></target> |         <target state="final">Добавлено: <x id="PH" equiv-text="countSuccess"/></target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="760986369763309193" datatype="html"> |       <trans-unit id="760986369763309193" datatype="html" approved="yes"> | ||||||
|         <source>, </source> |         <source>, </source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts</context> |           <context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts</context> | ||||||
| @@ -678,7 +678,7 @@ | |||||||
|           <context context-type="linenumber">117</context> |           <context context-type="linenumber">117</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note> |         <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note> | ||||||
|         <target state="needs-translation">, </target> |         <target state="final">, </target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3852289441366561594" datatype="html" approved="yes"> |       <trans-unit id="3852289441366561594" datatype="html" approved="yes"> | ||||||
|         <source>Connecting...</source> |         <source>Connecting...</source> | ||||||
| @@ -1222,7 +1222,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">117</context> |           <context context-type="linenumber">117</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">"<x id="PH" equiv-text="items[0].name"/>"</target> |         <target state="translated">"<x id="PH" equiv-text="items[0].name"/>"</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8639884465898458690" datatype="html" approved="yes"> |       <trans-unit id="8639884465898458690" datatype="html" approved="yes"> | ||||||
|         <source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source> |         <source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source> | ||||||
| @@ -2184,7 +2184,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">123</context> |           <context context-type="linenumber">123</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="final">Показывать уведомления, когда новый документ удалён</target> |         <target state="final">Показывать уведомления при обнаружении новых документов</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6057053428592387613" datatype="html" approved="yes"> |       <trans-unit id="6057053428592387613" datatype="html" approved="yes"> | ||||||
|         <source>Show notifications when document processing completes successfully</source> |         <source>Show notifications when document processing completes successfully</source> | ||||||
| @@ -2452,7 +2452,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> |           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> | ||||||
|           <context context-type="linenumber">97</context> |           <context context-type="linenumber">97</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">You have unsaved changes.</target> |         <target state="translated">У вас есть несохраненные изменения.</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3305084982600522070" datatype="html"> |       <trans-unit id="3305084982600522070" datatype="html"> | ||||||
|         <source>Are you sure you want to leave?</source> |         <source>Are you sure you want to leave?</source> | ||||||
| @@ -2468,7 +2468,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/guards/dirty-form.guard.ts</context> |           <context context-type="sourcefile">src/app/guards/dirty-form.guard.ts</context> | ||||||
|           <context context-type="linenumber">20</context> |           <context context-type="linenumber">20</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Leave page</target> |         <target state="translated">Покинуть страницу</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7536524521722799066" datatype="html" approved="yes"> |       <trans-unit id="7536524521722799066" datatype="html" approved="yes"> | ||||||
|         <source>(no title)</source> |         <source>(no title)</source> | ||||||
| @@ -2608,7 +2608,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> |           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> | ||||||
|           <context context-type="linenumber">77</context> |           <context context-type="linenumber">77</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Are you sure you want to close this document?</target> |         <target state="translated">Вы уверены, что хотите закрыть этот документ?</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2885986061416655600" datatype="html"> |       <trans-unit id="2885986061416655600" datatype="html"> | ||||||
|         <source>Close document</source> |         <source>Close document</source> | ||||||
| @@ -2616,7 +2616,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> |           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> | ||||||
|           <context context-type="linenumber">79</context> |           <context context-type="linenumber">79</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Close document</target> |         <target state="translated">Закрыть документ</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6755718693176327396" datatype="html"> |       <trans-unit id="6755718693176327396" datatype="html"> | ||||||
|         <source>Are you sure you want to close all documents?</source> |         <source>Are you sure you want to close all documents?</source> | ||||||
| @@ -2624,7 +2624,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> |           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> | ||||||
|           <context context-type="linenumber">98</context> |           <context context-type="linenumber">98</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Are you sure you want to close all documents?</target> |         <target state="translated">Вы уверены, что хотите закрыть все документы?</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4215561719980781894" datatype="html"> |       <trans-unit id="4215561719980781894" datatype="html"> | ||||||
|         <source>Close documents</source> |         <source>Close documents</source> | ||||||
| @@ -2632,7 +2632,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> |           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> | ||||||
|           <context context-type="linenumber">100</context> |           <context context-type="linenumber">100</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Close documents</target> |         <target state="translated">Закрыть документы</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3553216189604488439" datatype="html" approved="yes"> |       <trans-unit id="3553216189604488439" datatype="html" approved="yes"> | ||||||
|         <source>Modified</source> |         <source>Modified</source> | ||||||
| @@ -2721,7 +2721,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/settings.service.ts</context> |           <context context-type="sourcefile">src/app/services/settings.service.ts</context> | ||||||
|           <context context-type="linenumber">98</context> |           <context context-type="linenumber">98</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Luxembourgish</target> |         <target state="translated">Люксембургский</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3071065188816255493" datatype="html" approved="yes"> |       <trans-unit id="3071065188816255493" datatype="html" approved="yes"> | ||||||
|         <source>Dutch</source> |         <source>Dutch</source> | ||||||
| @@ -2785,7 +2785,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/settings.service.ts</context> |           <context context-type="sourcefile">src/app/services/settings.service.ts</context> | ||||||
|           <context context-type="linenumber">115</context> |           <context context-type="linenumber">115</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">ISO 8601</target> |         <target state="translated">ISO 8601</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1519954996184640001" datatype="html" approved="yes"> |       <trans-unit id="1519954996184640001" datatype="html" approved="yes"> | ||||||
|         <source>Error</source> |         <source>Error</source> | ||||||
|   | |||||||
| @@ -137,7 +137,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context> | ||||||
|           <context context-type="linenumber">38</context> |           <context context-type="linenumber">38</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Dokumenti</target> |         <target state="translated">Dokumenta</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="472206565520537964" datatype="html"> |       <trans-unit id="472206565520537964" datatype="html"> | ||||||
|         <source>Saved views</source> |         <source>Saved views</source> | ||||||
| @@ -157,7 +157,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> |           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||||
|           <context context-type="linenumber">87</context> |           <context context-type="linenumber">87</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Otvoreni dokumenti</target> |         <target state="translated">Otvorena dokumenta</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5687256342387781369" datatype="html"> |       <trans-unit id="5687256342387781369" datatype="html"> | ||||||
|         <source>Close all</source> |         <source>Close all</source> | ||||||
| @@ -511,7 +511,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> |           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> | ||||||
|           <context context-type="linenumber">33</context> |           <context context-type="linenumber">33</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Pozdrav <x id="PH" equiv-text="this.displayName"/>, dobro došao Paperless-ngx!</target> |         <target state="translated">Pozdrav <x id="PH" equiv-text="this.displayName"/>, dobro došao u Paperless-ngx!</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="795745990148149834" datatype="html"> |       <trans-unit id="795745990148149834" datatype="html"> | ||||||
|         <source>Welcome to Paperless-ngx!</source> |         <source>Welcome to Paperless-ngx!</source> | ||||||
| @@ -922,7 +922,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">18</context> |           <context context-type="linenumber">18</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Dopisnici</target> |         <target state="translated">Dopisnik</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5066119607229701477" datatype="html"> |       <trans-unit id="5066119607229701477" datatype="html"> | ||||||
|         <source>Document type</source> |         <source>Document type</source> | ||||||
| @@ -1488,7 +1488,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> | ||||||
|           <context context-type="linenumber">39</context> |           <context context-type="linenumber">39</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Sortirati</target> |         <target state="translated">Sortiraj</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2123659921722214537" datatype="html"> |       <trans-unit id="2123659921722214537" datatype="html"> | ||||||
|         <source>Views</source> |         <source>Views</source> | ||||||
|   | |||||||
| @@ -101,7 +101,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> |           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||||
|           <context context-type="linenumber">45</context> |           <context context-type="linenumber">45</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">注销</target> |         <target state="translated">退出</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6570363013146073520" datatype="html"> |       <trans-unit id="6570363013146073520" datatype="html"> | ||||||
|         <source>Dashboard</source> |         <source>Dashboard</source> | ||||||
| @@ -241,7 +241,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> |           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||||
|           <context context-type="linenumber">154</context> |           <context context-type="linenumber">154</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">管理</target> |         <target state="translated">后台管理</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="314315645942131479" datatype="html"> |       <trans-unit id="314315645942131479" datatype="html"> | ||||||
|         <source>Info</source> |         <source>Info</source> | ||||||
| @@ -257,7 +257,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> |           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||||
|           <context context-type="linenumber">167</context> |           <context context-type="linenumber">167</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">文档</target> |         <target state="translated">帮助文档</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1534029177398918729" datatype="html"> |       <trans-unit id="1534029177398918729" datatype="html"> | ||||||
|         <source>GitHub</source> |         <source>GitHub</source> | ||||||
| @@ -641,7 +641,7 @@ | |||||||
|           <context context-type="linenumber">25</context> |           <context context-type="linenumber">25</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <note priority="1" from="description">This is shown as a summary line when there are more than 5 document in the processing pipeline.</note> |         <note priority="1" from="description">This is shown as a summary line when there are more than 5 document in the processing pipeline.</note> | ||||||
|         <target state="translated">{VAR_PLURAL, plural, =1 {还有一个文档} other {<x id="INTERPOLATION"/> 更多文档}}</target> |         <target state="translated">{VAR_PLURAL, plural, =1 {还有一个文档} other {<x id="INTERPOLATION"/> 个更多文档}}</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6443586946875325554" datatype="html"> |       <trans-unit id="6443586946875325554" datatype="html"> | ||||||
|         <source>Processing: <x id="PH" equiv-text="countUploadingAndProcessing"/></source> |         <source>Processing: <x id="PH" equiv-text="countUploadingAndProcessing"/></source> | ||||||
| @@ -1256,7 +1256,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">130</context> |           <context context-type="linenumber">130</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将把标签“<x id="PH" equiv-text="tag.name"/>”添加到 <x id="PH_1" equiv-text="this.list.selected.size"/> 选定的文档。</target> |         <target state="translated">此操作将把标签“<x id="PH" equiv-text="tag.name"/>”添加到 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1894412783609570695" datatype="html"> |       <trans-unit id="1894412783609570695" datatype="html"> | ||||||
|         <source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> |         <source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> | ||||||
| @@ -1264,7 +1264,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">132</context> |           <context context-type="linenumber">132</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将添加标签 <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> 到 <x id="PH_1" equiv-text="this.list.selected.size"/> 选定的文档。</target> |         <target state="translated">此操作将添加标签 <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> 到 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7181166515756808573" datatype="html"> |       <trans-unit id="7181166515756808573" datatype="html"> | ||||||
|         <source>This operation will remove the tag "<x id="PH" equiv-text="tag.name"/>" from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> |         <source>This operation will remove the tag "<x id="PH" equiv-text="tag.name"/>" from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> | ||||||
| @@ -1272,7 +1272,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">135</context> |           <context context-type="linenumber">135</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将从 <x id="PH_1" equiv-text="this.list.selected.size"/> 选定的文档中移除标签“<x id="PH" equiv-text="tag.name"/>”。</target> |         <target state="translated">此操作将从 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档中移除标签“<x id="PH" equiv-text="tag.name"/>”。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3819792277998068944" datatype="html"> |       <trans-unit id="3819792277998068944" datatype="html"> | ||||||
|         <source>This operation will remove the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> |         <source>This operation will remove the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> | ||||||
| @@ -1280,7 +1280,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">137</context> |           <context context-type="linenumber">137</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将从 <x id="PH_1" equiv-text="this.list.selected.size"/> 选定的文档中删除标签 <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/>。</target> |         <target state="translated">此操作将从 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档中删除标签 <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/>。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2739066218579571288" datatype="html"> |       <trans-unit id="2739066218579571288" datatype="html"> | ||||||
|         <source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> and remove the tags <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source> |         <source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> and remove the tags <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source> | ||||||
| @@ -1288,7 +1288,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">139</context> |           <context context-type="linenumber">139</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将在指定的文档添加标签 <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> 并删除标签 <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/>。</target> |         <target state="translated">此操作将在 <x id="PH_2" equiv-text="this.list.selected.size"/> 个指定的文档添加标签 <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> 并删除标签 <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/>。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2996713129519325161" datatype="html"> |       <trans-unit id="2996713129519325161" datatype="html"> | ||||||
|         <source>Confirm correspondent assignment</source> |         <source>Confirm correspondent assignment</source> | ||||||
| @@ -1304,7 +1304,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">161</context> |           <context context-type="linenumber">161</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将分配联系人 "<x id="PH" equiv-text="correspondent.name"/>" 到 <x id="PH_1" equiv-text="this.list.selected.size"/> 选定的文档。</target> |         <target state="translated">此操作将分配联系人 "<x id="PH" equiv-text="correspondent.name"/>" 到 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1257522660364398440" datatype="html"> |       <trans-unit id="1257522660364398440" datatype="html"> | ||||||
|         <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> |         <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> | ||||||
| @@ -1328,7 +1328,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
|           <context context-type="linenumber">184</context> |           <context context-type="linenumber">184</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">此操作将将文档类型 "<x id="PH" equiv-text="documentType.name"/> 分配到 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档。</target> |         <target state="translated">此操作将把文档类型 "<x id="PH" equiv-text="documentType.name"/> 分配到 <x id="PH_1" equiv-text="this.list.selected.size"/> 个选定的文档。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2236642492594872779" datatype="html"> |       <trans-unit id="2236642492594872779" datatype="html"> | ||||||
|         <source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> |         <source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> | ||||||
| @@ -1984,7 +1984,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context> | ||||||
|           <context context-type="linenumber">104</context> |           <context context-type="linenumber">104</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">关联的文档将不会被删除。</target> |         <target state="translated">已关联的文档将不会被删除。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5467489005440577210" datatype="html"> |       <trans-unit id="5467489005440577210" datatype="html"> | ||||||
|         <source>Error while deleting element: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source> |         <source>Error while deleting element: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source> | ||||||
| @@ -2128,7 +2128,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">99</context> |           <context context-type="linenumber">99</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">在暗色模式下反转缩略图</target> |         <target state="translated">在深色模式下反转缩略图</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8508424367627989968" datatype="html"> |       <trans-unit id="8508424367627989968" datatype="html"> | ||||||
|         <source>Bulk editing</source> |         <source>Bulk editing</source> | ||||||
| @@ -2216,7 +2216,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">126</context> |           <context context-type="linenumber">126</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">这将禁止所有关于仪表盘文件处理状态的消息。</target> |         <target state="translated">这将禁止仪表盘上所有有关文件处理状态的消息。</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6925788033494878061" datatype="html"> |       <trans-unit id="6925788033494878061" datatype="html"> | ||||||
|         <source>Appears on</source> |         <source>Appears on</source> | ||||||
| @@ -2332,7 +2332,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/not-found/not-found.component.html</context> |           <context context-type="sourcefile">src/app/components/not-found/not-found.component.html</context> | ||||||
|           <context context-type="linenumber">7</context> |           <context context-type="linenumber">7</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">页面未找到</target> |         <target state="translated">404 页面未找到</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5851669019930456395" datatype="html"> |       <trans-unit id="5851669019930456395" datatype="html"> | ||||||
|         <source>Any word</source> |         <source>Any word</source> | ||||||
| @@ -2348,7 +2348,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/data/matching-model.ts</context> |           <context context-type="sourcefile">src/app/data/matching-model.ts</context> | ||||||
|           <context context-type="linenumber">12</context> |           <context context-type="linenumber">12</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">任意:文档包含其中任何一个单词(空间分隔)</target> |         <target state="translated">任意:文档包含其中任何一个单词(空格分隔)</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="700315718208181326" datatype="html"> |       <trans-unit id="700315718208181326" datatype="html"> | ||||||
|         <source>All words</source> |         <source>All words</source> | ||||||
|   | |||||||
| @@ -7,24 +7,112 @@ $enable-negative-margins: true; | |||||||
| @import "theme_dark"; | @import "theme_dark"; | ||||||
| @import "print"; | @import "print"; | ||||||
|  |  | ||||||
| .toolbaricon { | // Paperless-ngx styles | ||||||
|   width: 1.2em; |  | ||||||
|   height: 1.2em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .buttonicon { |  | ||||||
|   width: 1.2em; |  | ||||||
|   height: 1.2em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .sidebaricon { |  | ||||||
|   width: 16px; |  | ||||||
|   height: 16px; |  | ||||||
|   vertical-align: text-bottom; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| body { | body { | ||||||
|   font-size: 0.875rem; |   font-size: 0.875rem; | ||||||
|  |   height: 100vh; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | * { | ||||||
|  |   transition: background-color 0.3s ease, border-color 0.3s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | svg.logo { | ||||||
|  |   .leaf { | ||||||
|  |     fill: var(--bs-primary) !important; | ||||||
|  |   } | ||||||
|  |   .text { | ||||||
|  |     fill: var(--bs-body-color) !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .nav-link, .list-group-item { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .bg-body { | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .bg-primary { | ||||||
|  |   background-color: var(--bs-primary) !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn-primary { | ||||||
|  |   background-color: var(--bs-primary); | ||||||
|  |   border-color: var(--bs-primary); | ||||||
|  |  | ||||||
|  |   &:hover, &:focus { | ||||||
|  |     background-color: var(--pngx-primary-darken-10); | ||||||
|  |     border-color: var(--pngx-primary-darken-10); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &:disabled, &.disabled { | ||||||
|  |     background-color: var(--pngx-primary-darken-10) !important; | ||||||
|  |     border-color: var(--pngx-primary-darken-10) !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn-outline-primary { | ||||||
|  |   border-color: var(--bs-primary) !important; | ||||||
|  |   color: var(--bs-primary) !important; | ||||||
|  |  | ||||||
|  |   &:hover, &:focus, &.active, &:active { | ||||||
|  |     background-color: var(--bs-primary) !important; | ||||||
|  |     color: var(--bs-light) !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn-outline-secondary { | ||||||
|  |   color: var(--bs-secondary); | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     color: var(--bs-light); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .nav-item .sidebaricon { | ||||||
|  |   color: var(--bs-secondary); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn:focus, | ||||||
|  | .btn:active:focus, | ||||||
|  | .dropdown-item:focus, | ||||||
|  | .btn-check:focus + .btn, | ||||||
|  | .form-control:focus, | ||||||
|  | .form-check-input:focus, | ||||||
|  | .form-check-radio:focus, | ||||||
|  | .form-select:focus { | ||||||
|  |   box-shadow: 0 0 0 0.25rem hsla(var(--pngx-primary), var(--pngx-primary-lightness), var(--pngx-focus-alpha)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .form-switch .form-check-input:focus { | ||||||
|  |   background-image: escape-svg(url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#bbb'/></svg>")); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .nav-link:focus-visible, .nav-item a:focus-visible { | ||||||
|  |   outline: none; | ||||||
|  |   background-color: var(--pngx-bg-darker); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a.navbar-brand:focus-visible { | ||||||
|  |   outline: none; | ||||||
|  |   color: var(--pngx-primary-darken-10); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dropdown.show { | ||||||
|  |   > .btn-primary { | ||||||
|  |     background-color: var(--bs-primary); | ||||||
|  |     border-color: var(--bs-primary); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   > .btn-outline-primary { | ||||||
|  |     color: var(--pngx-primary-text-contrast) !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a, a:hover, .btn-link, .btn-link:hover { | ||||||
|  |   color: var(--bs-primary); | ||||||
| } | } | ||||||
|  |  | ||||||
| .form-control-dark { | .form-control-dark { | ||||||
| @@ -116,6 +204,245 @@ body { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .form-control:not(.btn), | ||||||
|  | input, | ||||||
|  | select, | ||||||
|  | textarea, | ||||||
|  | .form-select:not(.is-invalid):not(:disabled), | ||||||
|  | .form-check-input { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  |   border-color: var(--bs-border-color); | ||||||
|  |  | ||||||
|  |   &:focus { | ||||||
|  |     background-color: var(--pngx-bg-darker); | ||||||
|  |     color: var(--bs-body-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .form-check-input:checked { | ||||||
|  |   background-color: var(--bs-primary); | ||||||
|  |   border-color: var(--bs-primary); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .form-check-input:focus { | ||||||
|  |   border-color: var(--bs-primary); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .page-link { | ||||||
|  |   color: var(--bs-secondary); | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  |   border-color: var(--bs-border-color) !important; | ||||||
|  |  | ||||||
|  |   &:hover, &:focus { | ||||||
|  |     background-color: var(--bs-primary) !important; | ||||||
|  |     color: var(--bs-light) !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .page-item.active .page-link { | ||||||
|  |   background-color: var(--bs-primary); | ||||||
|  |   border-color: var(--bs-primary) !important; | ||||||
|  |   color: var(--bs-light); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .page-item.disabled .page-link { | ||||||
|  |   background-color: var(--pngx-bg-darker); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .nav-tabs { | ||||||
|  |   border-bottom: 1px solid var(--bs-border-color); | ||||||
|  |  | ||||||
|  |   .nav-link { | ||||||
|  |     color: var(--bs-primary); | ||||||
|  |  | ||||||
|  |     &.active, &:hover { | ||||||
|  |       border-color: var(--bs-border-color); | ||||||
|  |       background-color: var(--bs-body-bg); | ||||||
|  |       color: var(--bs-body-color); | ||||||
|  |       border-bottom: 1px solid transparent; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:focus { | ||||||
|  |       border-color: var(--bs-border-color); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active:focus, &:active { | ||||||
|  |       border-bottom: 1px solid transparent; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ng-select-container, | ||||||
|  | .ng-select.ng-select-opened > .ng-select-container, | ||||||
|  | .ng-dropdown-panel, | ||||||
|  | .ng-dropdown-panel .ng-dropdown-panel-items .ng-option { | ||||||
|  |   background-color: var(--bs-body-bg) !important; | ||||||
|  |   color: var(--bs-body-color) !important; | ||||||
|  |   border-color: var(--bs-border-color) !important; | ||||||
|  |  | ||||||
|  |   input:focus { | ||||||
|  |     background-color: transparent !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .input-group-text { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  |   border-color: var(--bs-border-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .list-group-item { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  |   border-color: var(--bs-border-color); | ||||||
|  |  | ||||||
|  |   &:hover, &:focus { | ||||||
|  |     background-color: var(--bs-body-bg); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dropdown-menu { | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  |  | ||||||
|  |   .dropdown-divider { | ||||||
|  |     border-color: var(--bs-border-color); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .dropdown-item { | ||||||
|  |     color: var(--bs-body-color); | ||||||
|  |  | ||||||
|  |     &:hover, &:focus { | ||||||
|  |       background-color: var(--pngx-bg-darker); | ||||||
|  |       color: var(--bs-body-color); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active { | ||||||
|  |       background-color: var(--bs-primary); | ||||||
|  |       color: var(--pngx-primary-text-contrast); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // icons | ||||||
|  | .toolbaricon { | ||||||
|  |   width: 1.2em; | ||||||
|  |   height: 1.2em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .buttonicon { | ||||||
|  |   width: 1.2em; | ||||||
|  |   height: 1.2em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .sidebaricon { | ||||||
|  |   width: 16px; | ||||||
|  |   height: 16px; | ||||||
|  |   vertical-align: text-bottom; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | table.table { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  |  | ||||||
|  |   .des,.asc { | ||||||
|  |     background-color: var(--bs-body-bg) !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .close { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal .btn-close { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .main-dropzone { | ||||||
|  |   height: 100%; | ||||||
|  |   width: 100%; | ||||||
|  |  | ||||||
|  |   &.ngx-file-drop__drop-zone--over { | ||||||
|  |     background-color: transparent !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .global-dropzone-overlay { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0; | ||||||
|  |   right: 0; | ||||||
|  |   bottom: 0; | ||||||
|  |   left: 0; | ||||||
|  |   background-color: rgba(23, 84, 31, .8); | ||||||
|  |   z-index: 1055; // $zindex-modal | ||||||
|  |   pointer-events: none !important; | ||||||
|  |   user-select: none !important; | ||||||
|  |   text-align: center; | ||||||
|  |   padding-top: 25%; | ||||||
|  |  | ||||||
|  |   &.show { | ||||||
|  |     opacity: 1 !important; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &.hide { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ngx-file-drop__drop-zone--over .global-dropzone-overlay { | ||||||
|  |   opacity: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inert { | ||||||
|  |   pointer-events: none !important; | ||||||
|  |   user-select: none !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .alert-danger { | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  |   background-color: var(--bs-danger); | ||||||
|  |   border-color: var(--bs-danger); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .alert-secondary { | ||||||
|  |   background-color: var(--pngx-primary-darken-18); | ||||||
|  |   border-color: var(--pngx-primary-darken-15); | ||||||
|  |   color: var(--bs-body-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ngb-dp-header, | ||||||
|  | .ngb-dp-weekdays, | ||||||
|  | .ngb-dp-month { | ||||||
|  |   background-color: var(--bs-body-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .popover { | ||||||
|  |   .popover-header, | ||||||
|  |   .popover-body { | ||||||
|  |     background-color: var(--pngx-bg-alt); | ||||||
|  |     border-color: var(--bs-border-color); | ||||||
|  |     color: var(--bs-body-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // fix popover carat colors | ||||||
|  | .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] { | ||||||
|  |   border-left-color: var(--pngx-bg-alt); | ||||||
|  | } | ||||||
|  | .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] { | ||||||
|  |   border-right-color: var(--pngx-bg-alt); | ||||||
|  | } | ||||||
|  | .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] { | ||||||
|  |   border-top-color: var(--pngx-bg-alt); | ||||||
|  | } | ||||||
|  | .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] { | ||||||
|  |   border-bottom-color: var(--pngx-bg-alt); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .bs-popover-bottom .popover-header::before, | ||||||
|  | .bs-popover-auto[x-placement^=bottom] .popover-header::before { | ||||||
|  |   border-bottom-color: var(--pngx-bg-alt); | ||||||
|  | } | ||||||
|  |  | ||||||
| // Bootstrap 5 tweaks | // Bootstrap 5 tweaks | ||||||
| a.badge { | a.badge { | ||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
|   | |||||||
| @@ -2,293 +2,16 @@ | |||||||
|   // base color e.g. #17541f = hsl(128, 57%, 21%) |   // base color e.g. #17541f = hsl(128, 57%, 21%) | ||||||
|   --pngx-primary: 128, 57%; |   --pngx-primary: 128, 57%; | ||||||
|   --pngx-primary-lightness: 21%; |   --pngx-primary-lightness: 21%; | ||||||
|  |   --pngx-primary-text-contrast: var(--bs-light); | ||||||
|  |  | ||||||
|   --bs-primary: hsl(var(--pngx-primary), var(--pngx-primary-lightness)); |   --bs-primary: hsl(var(--pngx-primary), var(--pngx-primary-lightness)); | ||||||
|   --bs-border-color: var(--bs-gray-400); |   --bs-border-color: var(--bs-gray-400); | ||||||
|   --ngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 72%)); |   --pngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 72%)); | ||||||
|   --ngx-primary-lighten-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%)); |   --pngx-primary-lighten-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%)); | ||||||
|   --ngx-primary-lighten-30: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 30%)); |   --pngx-primary-lighten-30: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 30%)); | ||||||
|   --ngx-primary-darken-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%)); |   --pngx-primary-darken-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%)); | ||||||
|   --ngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%)); |   --pngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%)); | ||||||
|   --ngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%)); |   --pngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%)); | ||||||
|   --ngx-bg-darker: var(--bs-gray-100); |   --pngx-bg-darker: var(--bs-gray-100); | ||||||
|   --ngx-focus-alpha: 0.3; |   --pngx-focus-alpha: 0.3; | ||||||
| } |  | ||||||
|  |  | ||||||
| svg.logo { |  | ||||||
|   .leaf { |  | ||||||
|     fill: var(--bs-primary) !important; |  | ||||||
|   } |  | ||||||
|   .text { |  | ||||||
|     fill: var(--bs-body-color) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .nav-link, .list-group-item { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .bg-body { |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .bg-primary { |  | ||||||
|   background-color: var(--bs-primary) !important; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .btn-primary { |  | ||||||
|   background-color: var(--bs-primary); |  | ||||||
|   border-color: var(--bs-primary); |  | ||||||
|  |  | ||||||
|   &:hover, &:focus { |  | ||||||
|     background-color: var(--ngx-primary-darken-10); |  | ||||||
|     border-color: var(--ngx-primary-darken-10); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:disabled, &.disabled { |  | ||||||
|     background-color: var(--ngx-primary-darken-10) !important; |  | ||||||
|     border-color: var(--ngx-primary-darken-10) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .btn-outline-primary { |  | ||||||
|   border-color: var(--bs-primary) !important; |  | ||||||
|   color: var(--bs-primary) !important; |  | ||||||
|  |  | ||||||
|   &:hover, &:focus, &.active, &:active { |  | ||||||
|     background-color: var(--bs-primary) !important; |  | ||||||
|     color: var(--bs-light) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .btn-outline-secondary { |  | ||||||
|   color: var(--bs-secondary); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .nav-item .sidebaricon { |  | ||||||
|   color: var(--bs-secondary); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .btn:focus, |  | ||||||
| .btn:active:focus, |  | ||||||
| .dropdown-item:focus, |  | ||||||
| .btn-check:focus + .btn, |  | ||||||
| .form-control:focus, |  | ||||||
| .form-check-input:focus, |  | ||||||
| .form-check-radio:focus, |  | ||||||
| .form-select:focus { |  | ||||||
|   box-shadow: 0 0 0 0.25rem hsla(var(--pngx-primary), var(--pngx-primary-lightness), var(--ngx-focus-alpha)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .form-switch .form-check-input:focus { |  | ||||||
|   background-image: escape-svg(url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#bbb'/></svg>")); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .nav-link:focus-visible, .nav-item a:focus-visible { |  | ||||||
|   outline: none; |  | ||||||
|   background-color: var(--ngx-bg-darker); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| a.navbar-brand:focus-visible { |  | ||||||
|   outline: none; |  | ||||||
|   color: var(--ngx-primary-darken-10); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dropdown.show { |  | ||||||
|   > .btn-primary { |  | ||||||
|     background-color: var(--bs-primary); |  | ||||||
|     border-color: var(--bs-primary); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   > .btn-outline-primary { |  | ||||||
|     color: var(--bs-body-color) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| a, a:hover, .btn-link, .btn-link:hover { |  | ||||||
|   color: var(--bs-primary); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .form-control:not(.btn), |  | ||||||
| input, |  | ||||||
| select, |  | ||||||
| textarea, |  | ||||||
| .form-select:not(.is-invalid):not(:disabled), |  | ||||||
| .form-check-input { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
|   border-color: var(--bs-border-color); |  | ||||||
|  |  | ||||||
|   &:focus { |  | ||||||
|     background-color: var(--ngx-bg-darker); |  | ||||||
|     color: var(--bs-body-color) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .form-check-input:checked { |  | ||||||
|   background-color: var(--bs-primary); |  | ||||||
|   border-color: var(--bs-primary); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .form-check-input:focus { |  | ||||||
|   border-color: var(--bs-primary); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .page-link { |  | ||||||
|   color: var(--bs-secondary); |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
|   border-color: var(--bs-border-color) !important; |  | ||||||
|  |  | ||||||
|   &:hover, &:focus { |  | ||||||
|     background-color: var(--bs-primary) !important; |  | ||||||
|     color: var(--bs-light) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .page-item.active .page-link { |  | ||||||
|   background-color: var(--bs-primary); |  | ||||||
|   border-color: var(--bs-primary) !important; |  | ||||||
|   color: var(--bs-light); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .page-item.disabled .page-link { |  | ||||||
|   background-color: var(--ngx-bg-darker); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .nav-tabs { |  | ||||||
|   border-bottom: 1px solid var(--bs-border-color); |  | ||||||
|  |  | ||||||
|   .nav-link { |  | ||||||
|     color: var(--bs-primary); |  | ||||||
|  |  | ||||||
|     &.active, &:hover { |  | ||||||
|       border-color: var(--bs-border-color); |  | ||||||
|       background-color: var(--bs-body-bg); |  | ||||||
|       color: var(--bs-body-color); |  | ||||||
|       border-bottom: 1px solid transparent; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &:focus { |  | ||||||
|       border-color: var(--bs-border-color); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &.active:focus, &:active { |  | ||||||
|       border-bottom: 1px solid transparent; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ng-select-container, |  | ||||||
| .ng-select.ng-select-opened > .ng-select-container, |  | ||||||
| .ng-dropdown-panel, |  | ||||||
| .ng-dropdown-panel .ng-dropdown-panel-items .ng-option { |  | ||||||
|   background-color: var(--bs-body-bg) !important; |  | ||||||
|   color: var(--bs-body-color) !important; |  | ||||||
|   border-color: var(--bs-border-color) !important; |  | ||||||
|  |  | ||||||
|   input:focus { |  | ||||||
|     background-color: transparent !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .input-group-text { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
|   border-color: var(--bs-border-color); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .list-group-item { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
|   border-color: var(--bs-border-color); |  | ||||||
|  |  | ||||||
|   &:hover, &:focus { |  | ||||||
|     background-color: var(--bs-body-bg); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dropdown-menu { |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
|  |  | ||||||
|   .dropdown-divider { |  | ||||||
|     border-color: var(--bs-border-color); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .dropdown-item { |  | ||||||
|     color: var(--bs-body-color); |  | ||||||
|  |  | ||||||
|     &:hover { |  | ||||||
|       background-color: var(--ngx-bg-darker); |  | ||||||
|       color: var(--bs-body-color); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &.active { |  | ||||||
|       background-color: var(--bs-primary); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.table { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
|  |  | ||||||
|   .des,.asc { |  | ||||||
|     background-color: var(--bs-body-bg) !important; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .close { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .modal .btn-close { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ngx-file-drop__drop-zone--over { |  | ||||||
|   background-color: var(--ngx-primary-faded) !important; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .alert-danger { |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
|   background-color: var(--bs-danger); |  | ||||||
|   border-color: var(--bs-danger); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .alert-secondary { |  | ||||||
|   background-color: var(--ngx-primary-darken-18); |  | ||||||
|   border-color: var(--ngx-primary-darken-15); |  | ||||||
|   color: var(--bs-body-color); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .ngb-dp-header, |  | ||||||
| .ngb-dp-weekdays, |  | ||||||
| .ngb-dp-month { |  | ||||||
|   background-color: var(--bs-body-bg); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .popover { |  | ||||||
|   .popover-header, |  | ||||||
|   .popover-body { |  | ||||||
|     background-color: var(--ngx-bg-alt); |  | ||||||
|     border-color: var(--bs-border-color); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // fix popover carat colors |  | ||||||
| .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] { |  | ||||||
|   border-left-color: var(--ngx-bg-alt); |  | ||||||
| } |  | ||||||
| .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] { |  | ||||||
|   border-right-color: var(--ngx-bg-alt); |  | ||||||
| } |  | ||||||
| .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] { |  | ||||||
|   border-top-color: var(--ngx-bg-alt); |  | ||||||
| } |  | ||||||
| .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] { |  | ||||||
|   border-bottom-color: var(--ngx-bg-alt); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .bs-popover-bottom .popover-header::before, |  | ||||||
| .bs-popover-auto[x-placement^=bottom] .popover-header::before { |  | ||||||
|   border-bottom-color: var(--ngx-bg-alt); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,10 +13,6 @@ $text-color-dark-mode: #abb2bf; | |||||||
| $text-color-dark-mode-accent: lighten($text-color-dark-mode, 10%); | $text-color-dark-mode-accent: lighten($text-color-dark-mode, 10%); | ||||||
| $border-color-dark-mode: #47494f; | $border-color-dark-mode: #47494f; | ||||||
|  |  | ||||||
| * { |  | ||||||
|   transition: background-color 0.3s ease, border-color 0.3s ease; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @mixin dark-mode { | @mixin dark-mode { | ||||||
|   --bs-primary: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%)); |   --bs-primary: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%)); | ||||||
|   --bs-danger: #{$danger-dark-mode}; |   --bs-danger: #{$danger-dark-mode}; | ||||||
| @@ -27,11 +23,12 @@ $border-color-dark-mode: #47494f; | |||||||
|   --bs-light: #{$bg-light-dark-mode}; |   --bs-light: #{$bg-light-dark-mode}; | ||||||
|   --bs-light-rgb: #{$bg-light-dark-mode-rgb}; |   --bs-light-rgb: #{$bg-light-dark-mode-rgb}; | ||||||
|   --bs-border-color: #{$border-color-dark-mode}; |   --bs-border-color: #{$border-color-dark-mode}; | ||||||
|   --ngx-bg-darker: #{$bg-dark-mode-accent}; |   --pngx-bg-darker: #{$bg-dark-mode-accent}; | ||||||
|   --ngx-bg-alt: #{$bg-dark-mode-alt}; |   --pngx-bg-alt: #{$bg-dark-mode-alt}; | ||||||
|   --ngx-body-color-accent: #{$text-color-dark-mode-accent}; |   --pngx-body-color-accent: #{$text-color-dark-mode-accent}; | ||||||
|   --ngx-focus-alpha: 0.7; |   --pngx-focus-alpha: 0.7; | ||||||
|   --ngx-primary-faded: var(--ngx-primary-darken-15); |   --pngx-primary-faded: var(--pngx-primary-darken-15); | ||||||
|  |   --pngx-primary-text-contrast: var(--bs-body-color); | ||||||
|  |  | ||||||
|   .navbar.bg-primary{ |   .navbar.bg-primary{ | ||||||
|     --bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness)); |     --bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness)); | ||||||
| @@ -64,17 +61,23 @@ $border-color-dark-mode: #47494f; | |||||||
|  |  | ||||||
|   .btn-outline-primary, .btn-primary { |   .btn-outline-primary, .btn-primary { | ||||||
|     &:hover, &:focus, &.active, &:active { |     &:hover, &:focus, &.active, &:active { | ||||||
|       color: var(--ngx-body-color-accent) !important; |       color: var(--bs-light) !important; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .btn-outline-secondary { |   .btn-outline-secondary { | ||||||
|     &:hover, &:focus, &.active, &:active { |     &:hover, &:focus, &.active, &:active { | ||||||
|       background-color: var(--ngx-bg-darker); |       background-color: var(--pngx-bg-darker); | ||||||
|       color: var(--bs-primary); |       color: var(--bs-primary); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   .search-form-container { | ||||||
|  |     input, input:focus { | ||||||
|  |       color: var(--bs-body-color) !important; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .card { |   .card { | ||||||
|     background-color: var(--bs-body-bg); |     background-color: var(--bs-body-bg); | ||||||
|  |  | ||||||
| @@ -141,11 +144,11 @@ $border-color-dark-mode: #47494f; | |||||||
|     color: $text-color-dark-mode-accent; |     color: $text-color-dark-mode-accent; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .close, .modal .btn-close { |   .close, .modal .btn-close, .alert .btn-close { | ||||||
|     text-shadow: 0 1px 0 #666; |     text-shadow: 0 1px 0 #666; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .modal .btn-close { |   .modal .btn-close, .alert .btn-close { | ||||||
|     filter: invert(1) grayscale(100%) brightness(200%); |     filter: invert(1) grayscale(100%) brightness(200%); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,9 @@ import os | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from pathlib import PurePath | from pathlib import PurePath | ||||||
| from threading import Thread | from threading import Thread | ||||||
|  | from time import monotonic | ||||||
| from time import sleep | from time import sleep | ||||||
|  | from typing import Final | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.management.base import BaseCommand | from django.core.management.base import BaseCommand | ||||||
| @@ -53,6 +55,25 @@ def _consume(filepath): | |||||||
|         logger.warning(f"Not consuming file {filepath}: Unknown file extension.") |         logger.warning(f"Not consuming file {filepath}: Unknown file extension.") | ||||||
|         return |         return | ||||||
|  |  | ||||||
|  |     # Total wait time: up to 500ms | ||||||
|  |     os_error_retry_count: Final[int] = 50 | ||||||
|  |     os_error_retry_wait: Final[float] = 0.01 | ||||||
|  |  | ||||||
|  |     read_try_count = 0 | ||||||
|  |     file_open_ok = False | ||||||
|  |  | ||||||
|  |     while (read_try_count < os_error_retry_count) and not file_open_ok: | ||||||
|  |         try: | ||||||
|  |             with open(filepath, "rb"): | ||||||
|  |                 file_open_ok = True | ||||||
|  |         except OSError: | ||||||
|  |             read_try_count += 1 | ||||||
|  |             sleep(os_error_retry_wait) | ||||||
|  |  | ||||||
|  |     if read_try_count >= os_error_retry_count: | ||||||
|  |         logger.warning(f"Not consuming file {filepath}: OS reports file as busy still") | ||||||
|  |         return | ||||||
|  |  | ||||||
|     tag_ids = None |     tag_ids = None | ||||||
|     try: |     try: | ||||||
|         if settings.CONSUMER_SUBDIRS_AS_TAGS: |         if settings.CONSUMER_SUBDIRS_AS_TAGS: | ||||||
| @@ -81,19 +102,23 @@ def _consume_wait_unmodified(file): | |||||||
|  |  | ||||||
|     logger.debug(f"Waiting for file {file} to remain unmodified") |     logger.debug(f"Waiting for file {file} to remain unmodified") | ||||||
|     mtime = -1 |     mtime = -1 | ||||||
|  |     size = -1 | ||||||
|     current_try = 0 |     current_try = 0 | ||||||
|     while current_try < settings.CONSUMER_POLLING_RETRY_COUNT: |     while current_try < settings.CONSUMER_POLLING_RETRY_COUNT: | ||||||
|         try: |         try: | ||||||
|             new_mtime = os.stat(file).st_mtime |             stat_data = os.stat(file) | ||||||
|  |             new_mtime = stat_data.st_mtime | ||||||
|  |             new_size = stat_data.st_size | ||||||
|         except FileNotFoundError: |         except FileNotFoundError: | ||||||
|             logger.debug( |             logger.debug( | ||||||
|                 f"File {file} moved while waiting for it to remain " f"unmodified.", |                 f"File {file} moved while waiting for it to remain " f"unmodified.", | ||||||
|             ) |             ) | ||||||
|             return |             return | ||||||
|         if new_mtime == mtime: |         if new_mtime == mtime and new_size == size: | ||||||
|             _consume(file) |             _consume(file) | ||||||
|             return |             return | ||||||
|         mtime = new_mtime |         mtime = new_mtime | ||||||
|  |         size = new_size | ||||||
|         sleep(settings.CONSUMER_POLLING_DELAY) |         sleep(settings.CONSUMER_POLLING_DELAY) | ||||||
|         current_try += 1 |         current_try += 1 | ||||||
|  |  | ||||||
| @@ -182,14 +207,32 @@ class Command(BaseCommand): | |||||||
|             descriptor = inotify.add_watch(directory, inotify_flags) |             descriptor = inotify.add_watch(directory, inotify_flags) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|  |  | ||||||
|  |             inotify_debounce: Final[float] = 0.5 | ||||||
|  |             notified_files = {} | ||||||
|  |  | ||||||
|             while not self.stop_flag: |             while not self.stop_flag: | ||||||
|  |  | ||||||
|                 for event in inotify.read(timeout=1000): |                 for event in inotify.read(timeout=1000): | ||||||
|                     if recursive: |                     if recursive: | ||||||
|                         path = inotify.get_path(event.wd) |                         path = inotify.get_path(event.wd) | ||||||
|                     else: |                     else: | ||||||
|                         path = directory |                         path = directory | ||||||
|                     filepath = os.path.join(path, event.name) |                     filepath = os.path.join(path, event.name) | ||||||
|                     _consume(filepath) |                     notified_files[filepath] = monotonic() | ||||||
|  |  | ||||||
|  |                 # Check the files against the timeout | ||||||
|  |                 still_waiting = {} | ||||||
|  |                 for filepath in notified_files: | ||||||
|  |                     # Time of the last inotify event for this file | ||||||
|  |                     last_event_time = notified_files[filepath] | ||||||
|  |                     if (monotonic() - last_event_time) > inotify_debounce: | ||||||
|  |                         _consume(filepath) | ||||||
|  |                     else: | ||||||
|  |                         still_waiting[filepath] = last_event_time | ||||||
|  |                 # These files are still waiting to hit the timeout | ||||||
|  |                 notified_files = still_waiting | ||||||
|  |  | ||||||
|         except KeyboardInterrupt: |         except KeyboardInterrupt: | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ def match_tags(document, classifier): | |||||||
| def matches(matching_model, document): | def matches(matching_model, document): | ||||||
|     search_kwargs = {} |     search_kwargs = {} | ||||||
|  |  | ||||||
|     document_content = document.content.lower() |     document_content = document.content | ||||||
|  |  | ||||||
|     # Check that match is not empty |     # Check that match is not empty | ||||||
|     if matching_model.match.strip() == "": |     if matching_model.match.strip() == "": | ||||||
|   | |||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | # Generated by Django 4.0.3 on 2022-04-01 22:50 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("documents", "1017_alter_savedviewfilterrule_rule_type"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="savedviewfilterrule", | ||||||
|  |             name="value", | ||||||
|  |             field=models.CharField( | ||||||
|  |                 blank=True, max_length=255, null=True, verbose_name="value" | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -375,7 +375,7 @@ class SavedViewFilterRule(models.Model): | |||||||
|  |  | ||||||
|     rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES) |     rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES) | ||||||
|  |  | ||||||
|     value = models.CharField(_("value"), max_length=128, blank=True, null=True) |     value = models.CharField(_("value"), max_length=255, blank=True, null=True) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("filter rule") |         verbose_name = _("filter rule") | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ from documents.signals import document_consumer_declaration | |||||||
| # - XX. MONTH ZZZZ with XX being 1 or 2 and ZZZZ being 2 or 4 digits | # - XX. MONTH ZZZZ with XX being 1 or 2 and ZZZZ being 2 or 4 digits | ||||||
| # - MONTH ZZZZ, with ZZZZ being 4 digits | # - MONTH ZZZZ, with ZZZZ being 4 digits | ||||||
| # - MONTH XX, ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits | # - MONTH XX, ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits | ||||||
|  | # - XX MON ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits. MONTH is 3 letters | ||||||
|  |  | ||||||
| # TODO: isnt there a date parsing library for this? | # TODO: isnt there a date parsing library for this? | ||||||
|  |  | ||||||
| @@ -31,7 +32,8 @@ DATE_REGEX = re.compile( | |||||||
|     r"(\b|(?!=([_-])))([0-9]{4}|[0-9]{2})[\.\/-]([0-9]{1,2})[\.\/-]([0-9]{1,2})(\b|(?=([_-])))|"  # noqa: E501 |     r"(\b|(?!=([_-])))([0-9]{4}|[0-9]{2})[\.\/-]([0-9]{1,2})[\.\/-]([0-9]{1,2})(\b|(?=([_-])))|"  # noqa: E501 | ||||||
|     r"(\b|(?!=([_-])))([0-9]{1,2}[\. ]+[^ ]{3,9} ([0-9]{4}|[0-9]{2}))(\b|(?=([_-])))|"  # noqa: E501 |     r"(\b|(?!=([_-])))([0-9]{1,2}[\. ]+[^ ]{3,9} ([0-9]{4}|[0-9]{2}))(\b|(?=([_-])))|"  # noqa: E501 | ||||||
|     r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{1,2}, ([0-9]{4}))(\b|(?=([_-])))|" |     r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{1,2}, ([0-9]{4}))(\b|(?=([_-])))|" | ||||||
|     r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))", |     r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))|" | ||||||
|  |     r"(\b|(?!=([_-])))(\b[0-9]{1,2}[ \.\/-][A-Z]{3}[ \.\/-][0-9]{4})(\b|(?=([_-])))",  # noqa: E501 | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
|  | import shutil | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.admin.models import ADDITION | from django.contrib.admin.models import ADDITION | ||||||
| @@ -252,7 +253,7 @@ def cleanup_document_deletion(sender, instance, using, **kwargs): | |||||||
|  |  | ||||||
|             logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}") |             logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}") | ||||||
|             try: |             try: | ||||||
|                 os.rename(instance.source_path, new_file_path) |                 shutil.move(instance.source_path, new_file_path) | ||||||
|             except OSError as e: |             except OSError as e: | ||||||
|                 logger.error( |                 logger.error( | ||||||
|                     f"Failed to move {instance.source_path} to trash at " |                     f"Failed to move {instance.source_path} to trash at " | ||||||
|   | |||||||
| @@ -1,6 +1,12 @@ | |||||||
| import logging | import logging | ||||||
|  | import os | ||||||
|  | import shutil | ||||||
|  | import tempfile | ||||||
|  | from typing import List  # for type hinting. Can be removed, if only Python >3.8 is used | ||||||
|  |  | ||||||
| import tqdm | import tqdm | ||||||
|  | from asgiref.sync import async_to_sync | ||||||
|  | from channels.layers import get_channel_layer | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db.models.signals import post_save | from django.db.models.signals import post_save | ||||||
| from documents import index | from documents import index | ||||||
| @@ -14,8 +20,12 @@ from documents.models import Document | |||||||
| from documents.models import DocumentType | from documents.models import DocumentType | ||||||
| from documents.models import Tag | from documents.models import Tag | ||||||
| from documents.sanity_checker import SanityCheckFailedException | from documents.sanity_checker import SanityCheckFailedException | ||||||
|  | from pdf2image import convert_from_path | ||||||
|  | from pikepdf import Pdf | ||||||
|  | from pyzbar import pyzbar | ||||||
| from whoosh.writing import AsyncWriter | from whoosh.writing import AsyncWriter | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger("paperless.tasks") | logger = logging.getLogger("paperless.tasks") | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -62,6 +72,115 @@ def train_classifier(): | |||||||
|         logger.warning("Classifier error: " + str(e)) |         logger.warning("Classifier error: " + str(e)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def barcode_reader(image) -> List[str]: | ||||||
|  |     """ | ||||||
|  |     Read any barcodes contained in image | ||||||
|  |     Returns a list containing all found barcodes | ||||||
|  |     """ | ||||||
|  |     barcodes = [] | ||||||
|  |     # Decode the barcode image | ||||||
|  |     detected_barcodes = pyzbar.decode(image) | ||||||
|  |  | ||||||
|  |     if detected_barcodes: | ||||||
|  |         # Traverse through all the detected barcodes in image | ||||||
|  |         for barcode in detected_barcodes: | ||||||
|  |             if barcode.data: | ||||||
|  |                 decoded_barcode = barcode.data.decode("utf-8") | ||||||
|  |                 barcodes.append(decoded_barcode) | ||||||
|  |                 logger.debug( | ||||||
|  |                     f"Barcode of type {str(barcode.type)} found: {decoded_barcode}", | ||||||
|  |                 ) | ||||||
|  |     return barcodes | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def scan_file_for_separating_barcodes(filepath: str) -> List[int]: | ||||||
|  |     """ | ||||||
|  |     Scan the provided file for page separating barcodes | ||||||
|  |     Returns a list of pagenumbers, which separate the file | ||||||
|  |     """ | ||||||
|  |     separator_page_numbers = [] | ||||||
|  |     separator_barcode = str(settings.CONSUMER_BARCODE_STRING) | ||||||
|  |     # use a temporary directory in case the file os too big to handle in memory | ||||||
|  |     with tempfile.TemporaryDirectory() as path: | ||||||
|  |         pages_from_path = convert_from_path(filepath, output_folder=path) | ||||||
|  |         for current_page_number, page in enumerate(pages_from_path): | ||||||
|  |             current_barcodes = barcode_reader(page) | ||||||
|  |             if separator_barcode in current_barcodes: | ||||||
|  |                 separator_page_numbers.append(current_page_number) | ||||||
|  |     return separator_page_numbers | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def separate_pages(filepath: str, pages_to_split_on: List[int]) -> List[str]: | ||||||
|  |     """ | ||||||
|  |     Separate the provided file on the pages_to_split_on. | ||||||
|  |     The pages which are defined by page_numbers will be removed. | ||||||
|  |     Returns a list of (temporary) filepaths to consume. | ||||||
|  |     These will need to be deleted later. | ||||||
|  |     """ | ||||||
|  |     os.makedirs(settings.SCRATCH_DIR, exist_ok=True) | ||||||
|  |     tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR) | ||||||
|  |     fname = os.path.splitext(os.path.basename(filepath))[0] | ||||||
|  |     pdf = Pdf.open(filepath) | ||||||
|  |     document_paths = [] | ||||||
|  |     logger.debug(f"Temp dir is {str(tempdir)}") | ||||||
|  |     if not pages_to_split_on: | ||||||
|  |         logger.warning("No pages to split on!") | ||||||
|  |     else: | ||||||
|  |         # go from the first page to the first separator page | ||||||
|  |         dst = Pdf.new() | ||||||
|  |         for n, page in enumerate(pdf.pages): | ||||||
|  |             if n < pages_to_split_on[0]: | ||||||
|  |                 dst.pages.append(page) | ||||||
|  |         output_filename = "{}_document_0.pdf".format(fname) | ||||||
|  |         savepath = os.path.join(tempdir, output_filename) | ||||||
|  |         with open(savepath, "wb") as out: | ||||||
|  |             dst.save(out) | ||||||
|  |         document_paths = [savepath] | ||||||
|  |  | ||||||
|  |         # iterate through the rest of the document | ||||||
|  |         for count, page_number in enumerate(pages_to_split_on): | ||||||
|  |             logger.debug(f"Count: {str(count)} page_number: {str(page_number)}") | ||||||
|  |             dst = Pdf.new() | ||||||
|  |             try: | ||||||
|  |                 next_page = pages_to_split_on[count + 1] | ||||||
|  |             except IndexError: | ||||||
|  |                 next_page = len(pdf.pages) | ||||||
|  |             # skip the first page_number. This contains the barcode page | ||||||
|  |             for page in range(page_number + 1, next_page): | ||||||
|  |                 logger.debug( | ||||||
|  |                     f"page_number: {str(page_number)} next_page: {str(next_page)}", | ||||||
|  |                 ) | ||||||
|  |                 dst.pages.append(pdf.pages[page]) | ||||||
|  |             output_filename = "{}_document_{}.pdf".format(fname, str(count + 1)) | ||||||
|  |             logger.debug(f"pdf no:{str(count)} has {str(len(dst.pages))} pages") | ||||||
|  |             savepath = os.path.join(tempdir, output_filename) | ||||||
|  |             with open(savepath, "wb") as out: | ||||||
|  |                 dst.save(out) | ||||||
|  |             document_paths.append(savepath) | ||||||
|  |     logger.debug(f"Temp files are {str(document_paths)}") | ||||||
|  |     return document_paths | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def save_to_dir( | ||||||
|  |     filepath: str, | ||||||
|  |     newname: str = None, | ||||||
|  |     target_dir: str = settings.CONSUMPTION_DIR, | ||||||
|  | ): | ||||||
|  |     """ | ||||||
|  |     Copies filepath to target_dir. | ||||||
|  |     Optionally rename the file. | ||||||
|  |     """ | ||||||
|  |     if os.path.isfile(filepath) and os.path.isdir(target_dir): | ||||||
|  |         dst = shutil.copy(filepath, target_dir) | ||||||
|  |         logging.debug(f"saved {str(filepath)} to {str(dst)}") | ||||||
|  |         if newname: | ||||||
|  |             dst_new = os.path.join(target_dir, newname) | ||||||
|  |             logger.debug(f"moving {str(dst)} to {str(dst_new)}") | ||||||
|  |             os.rename(dst, dst_new) | ||||||
|  |     else: | ||||||
|  |         logger.warning(f"{str(filepath)} or {str(target_dir)} don't exist.") | ||||||
|  |  | ||||||
|  |  | ||||||
| def consume_file( | def consume_file( | ||||||
|     path, |     path, | ||||||
|     override_filename=None, |     override_filename=None, | ||||||
| @@ -72,6 +191,48 @@ def consume_file( | |||||||
|     task_id=None, |     task_id=None, | ||||||
| ): | ): | ||||||
|  |  | ||||||
|  |     # check for separators in current document | ||||||
|  |     if settings.CONSUMER_ENABLE_BARCODES: | ||||||
|  |         separators = [] | ||||||
|  |         document_list = [] | ||||||
|  |         separators = scan_file_for_separating_barcodes(path) | ||||||
|  |         if separators: | ||||||
|  |             logger.debug(f"Pages with separators found in: {str(path)}") | ||||||
|  |             document_list = separate_pages(path, separators) | ||||||
|  |         if document_list: | ||||||
|  |             for n, document in enumerate(document_list): | ||||||
|  |                 # save to consumption dir | ||||||
|  |                 # rename it to the original filename  with number prefix | ||||||
|  |                 if override_filename: | ||||||
|  |                     newname = f"{str(n)}_" + override_filename | ||||||
|  |                 else: | ||||||
|  |                     newname = None | ||||||
|  |                 save_to_dir(document, newname=newname) | ||||||
|  |             # if we got here, the document was successfully split | ||||||
|  |             # and can safely be deleted | ||||||
|  |             logger.debug("Deleting file {}".format(path)) | ||||||
|  |             os.unlink(path) | ||||||
|  |             # notify the sender, otherwise the progress bar | ||||||
|  |             # in the UI stays stuck | ||||||
|  |             payload = { | ||||||
|  |                 "filename": override_filename, | ||||||
|  |                 "task_id": task_id, | ||||||
|  |                 "current_progress": 100, | ||||||
|  |                 "max_progress": 100, | ||||||
|  |                 "status": "SUCCESS", | ||||||
|  |                 "message": "finished", | ||||||
|  |             } | ||||||
|  |             try: | ||||||
|  |                 async_to_sync(get_channel_layer().group_send)( | ||||||
|  |                     "status_updates", | ||||||
|  |                     {"type": "status_update", "data": payload}, | ||||||
|  |                 ) | ||||||
|  |             except OSError as e: | ||||||
|  |                 logger.warning("OSError. It could be, the broker cannot be reached.") | ||||||
|  |                 logger.warning(str(e)) | ||||||
|  |             return "File successfully split" | ||||||
|  |  | ||||||
|  |     # continue with consumption if no barcode was found | ||||||
|     document = Consumer().try_consume_file( |     document = Consumer().try_consume_file( | ||||||
|         path, |         path, | ||||||
|         override_filename=override_filename, |         override_filename=override_filename, | ||||||
|   | |||||||
| @@ -23,8 +23,10 @@ | |||||||
| 		<script type="text/javascript"> | 		<script type="text/javascript"> | ||||||
| 			setTimeout(() => { | 			setTimeout(() => { | ||||||
| 				let warning = document.getElementsByClassName('warning').item(0) | 				let warning = document.getElementsByClassName('warning').item(0) | ||||||
| 				warning.classList.remove('hide') | 				if (warning) { | ||||||
| 				warning.classList.add('show') | 					warning.classList.remove('hide') | ||||||
|  | 					warning.classList.add('show') | ||||||
|  | 				} | ||||||
| 			}, 8000) | 			}, 8000) | ||||||
| 		</script> | 		</script> | ||||||
| 		<style type="text/css"> | 		<style type="text/css"> | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/barcodes/barcode-128-PATCHT.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 836 B | 
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/barcodes/barcode-128-custom.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/barcodes/barcode-128-custom.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 33 KiB | 
| After Width: | Height: | Size: 39 KiB | 
| After Width: | Height: | Size: 9.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/barcodes/barcode-39-PATCHT.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 891 B | 
							
								
								
									
										243
									
								
								src/documents/tests/samples/barcodes/barcode-39-custom.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/barcodes/barcode-39-custom.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.3 KiB | 
 phail
					phail