Merge branch 'dev' into fix-mail-starttls

This commit is contained in:
phail 2022-04-13 23:55:38 +02:00
commit 942b5aa9df
128 changed files with 11610 additions and 6203 deletions

View File

@ -17,3 +17,5 @@
**/htmlcov **/htmlcov
/src/.pytest_cache /src/.pytest_cache
.idea .idea
.venv/
.vscode/

View File

@ -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

View File

@ -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
<!-- <!--

View File

@ -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
View 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

View File

@ -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

View File

@ -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
View 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

View File

@ -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
View File

@ -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
View File

@ -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"
} }
} }
} }

View File

@ -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

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -e set -e

View File

@ -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:

View File

@ -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::

View File

@ -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.

View File

@ -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

View File

@ -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!

View File

@ -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
###################################### ######################################

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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/**/*

View File

@ -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",

View File

@ -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"}]}

View 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}]}

View 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"}]}

View File

@ -0,0 +1 @@
{"correspondents":[],"tags":[3],"document_types":[1]}

File diff suppressed because one or more lines are too long

View 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"}]}]}

View 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}]}

View 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')
})
})

View File

@ -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')
})
})

View 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')
})
})

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }
} }

View File

@ -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>

View File

@ -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)
}
} }

View File

@ -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,

View File

@ -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>&nbsp;<ng-container i18n>Documents</ng-container> </svg>&nbsp;<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>&nbsp;<ng-container i18n>GitHub</ng-container> </svg>&nbsp;<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>

View File

@ -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%;
}
}

View File

@ -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(),
},
]) ])
} }

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -0,0 +1,3 @@
a {
cursor: pointer;
}

View File

@ -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>

View File

@ -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
}

View File

@ -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;
}

View File

@ -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
}
}
}
)
})
}
}
} }
} }

View File

@ -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>

View File

@ -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%;
}

View File

@ -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'
} }
} }

View File

@ -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>&nbsp;<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>
&nbsp;
<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>

View File

@ -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
}) })
} }
} }

View File

@ -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}}

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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">

View File

@ -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;
}

View File

@ -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
},
})
}) })
} }

View File

@ -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"/>

View File

@ -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>

View File

@ -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 {

View File

@ -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) {

View 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/`
)
}
}

View File

@ -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`,

View 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()
},
})
})
}
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 861 KiB

File diff suppressed because it is too large Load Diff

View 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>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source> <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source>

File diff suppressed because it is too large Load Diff

View 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>

View File

@ -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>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source> <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</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>

View File

@ -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>

View File

@ -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 &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; 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>

View File

@ -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;

View File

@ -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);
} }

View File

@ -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%);
} }

View File

@ -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

View File

@ -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() == "":

View File

@ -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"
),
),
]

View File

@ -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")

View File

@ -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
) )

View File

@ -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 "

View File

@ -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,

View File

@ -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">

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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