mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Merge branch 'dev' into feature/slim-sidebar
This commit is contained in:
		@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "qpdf": {
 | 
					  "qpdf": {
 | 
				
			||||||
      "version": "10.6.3"
 | 
					      "version": "11.1.1"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  "jbig2enc": {
 | 
					  "jbig2enc": {
 | 
				
			||||||
      "version": "0.29",
 | 
					      "version": "0.29",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										37
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										37
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -82,6 +82,22 @@ jobs:
 | 
				
			|||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        python-version: ['3.8', '3.9', '3.10']
 | 
					        python-version: ['3.8', '3.9', '3.10']
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					    services:
 | 
				
			||||||
 | 
					      tika:
 | 
				
			||||||
 | 
					        image: ghcr.io/paperless-ngx/tika:latest
 | 
				
			||||||
 | 
					        ports:
 | 
				
			||||||
 | 
					          - "9998:9998/tcp"
 | 
				
			||||||
 | 
					      gotenberg:
 | 
				
			||||||
 | 
					        image: docker.io/gotenberg/gotenberg:7.4
 | 
				
			||||||
 | 
					        ports:
 | 
				
			||||||
 | 
					          - "3000:3000/tcp"
 | 
				
			||||||
 | 
					    env:
 | 
				
			||||||
 | 
					      # Enable Tika end to end testing
 | 
				
			||||||
 | 
					      TIKA_LIVE: 1
 | 
				
			||||||
 | 
					      # Enable paperless_mail testing against real server
 | 
				
			||||||
 | 
					      PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
 | 
				
			||||||
 | 
					      PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
 | 
				
			||||||
 | 
					      PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Checkout
 | 
					        name: Checkout
 | 
				
			||||||
@@ -91,7 +107,7 @@ jobs:
 | 
				
			|||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Install pipenv
 | 
					        name: Install pipenv
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          pipx install pipenv==2022.8.5
 | 
					          pipx install pipenv==2022.10.4
 | 
				
			||||||
          pipenv --version
 | 
					          pipenv --version
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Set up Python
 | 
					        name: Set up Python
 | 
				
			||||||
@@ -117,11 +133,11 @@ jobs:
 | 
				
			|||||||
        name: Tests
 | 
					        name: Tests
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          cd src/
 | 
					          cd src/
 | 
				
			||||||
          pipenv run pytest
 | 
					          pipenv run pytest -rfEp
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Get changed files
 | 
					        name: Get changed files
 | 
				
			||||||
        id: changed-files-specific
 | 
					        id: changed-files-specific
 | 
				
			||||||
        uses: tj-actions/changed-files@v29.0.2
 | 
					        uses: tj-actions/changed-files@v31.0.2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          files: |
 | 
					          files: |
 | 
				
			||||||
            src/**
 | 
					            src/**
 | 
				
			||||||
@@ -484,6 +500,18 @@ jobs:
 | 
				
			|||||||
        uses: actions/checkout@v3
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          ref: main
 | 
					          ref: main
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Install pipenv
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          pip3 install --upgrade pip setuptools wheel pipx
 | 
				
			||||||
 | 
					          pipx install pipenv
 | 
				
			||||||
 | 
					      -
 | 
				
			||||||
 | 
					        name: Set up Python
 | 
				
			||||||
 | 
					        uses: actions/setup-python@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: 3.9
 | 
				
			||||||
 | 
					          cache: "pipenv"
 | 
				
			||||||
 | 
					          cache-dependency-path: 'Pipfile.lock'
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Append Changelog to docs
 | 
					        name: Append Changelog to docs
 | 
				
			||||||
        id: append-Changelog
 | 
					        id: append-Changelog
 | 
				
			||||||
@@ -497,9 +525,10 @@ jobs:
 | 
				
			|||||||
          CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
 | 
					          CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
 | 
				
			||||||
          echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
 | 
					          echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
 | 
				
			||||||
          mv changelog-new.md changelog.md
 | 
					          mv changelog-new.md changelog.md
 | 
				
			||||||
 | 
					          pipenv run pre-commit --files changelog.md
 | 
				
			||||||
          git config --global user.name "github-actions"
 | 
					          git config --global user.name "github-actions"
 | 
				
			||||||
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
 | 
					          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
 | 
				
			||||||
          git commit -am "Changelog ${{ steps.get_version.outputs.version }} - GHA"
 | 
					          git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
 | 
				
			||||||
          git push origin ${{ needs.publish-release.outputs.version }}-changelog
 | 
					          git push origin ${{ needs.publish-release.outputs.version }}-changelog
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Create Pull Request
 | 
					        name: Create Pull Request
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/cleanup-tags.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cleanup-tags.yml
									
									
									
									
										vendored
									
									
								
							@@ -39,7 +39,7 @@ jobs:
 | 
				
			|||||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
					          password: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
        name: Set up Python
 | 
					        name: Set up Python
 | 
				
			||||||
        uses: actions/setup-python@v3
 | 
					        uses: actions/setup-python@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: "3.10"
 | 
					          python-version: "3.10"
 | 
				
			||||||
      -
 | 
					      -
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -38,7 +38,7 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
    - name: Checkout repository
 | 
					    - name: Checkout repository
 | 
				
			||||||
      uses: actions/checkout@v2
 | 
					      uses: actions/checkout@v3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Initializes the CodeQL tools for scanning.
 | 
					    # Initializes the CodeQL tools for scanning.
 | 
				
			||||||
    - name: Initialize CodeQL
 | 
					    - name: Initialize CodeQL
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.github/workflows/project-actions.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/project-actions.yml
									
									
									
									
										vendored
									
									
								
							@@ -28,7 +28,7 @@ jobs:
 | 
				
			|||||||
    if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
 | 
					    if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Add issue to project and set status to ${{ env.todo }}
 | 
					      - name: Add issue to project and set status to ${{ env.todo }}
 | 
				
			||||||
        uses: leonsteinhaeuser/project-beta-automations@v1.3.0
 | 
					        uses: leonsteinhaeuser/project-beta-automations@v2.0.1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          gh_token: ${{ secrets.GH_TOKEN }}
 | 
					          gh_token: ${{ secrets.GH_TOKEN }}
 | 
				
			||||||
          organization: paperless-ngx
 | 
					          organization: paperless-ngx
 | 
				
			||||||
@@ -44,7 +44,7 @@ jobs:
 | 
				
			|||||||
    if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
 | 
					    if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Add PR to project and set status to "Needs Review"
 | 
					      - name: Add PR to project and set status to "Needs Review"
 | 
				
			||||||
        uses: leonsteinhaeuser/project-beta-automations@v1.3.0
 | 
					        uses: leonsteinhaeuser/project-beta-automations@v2.0.1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          gh_token: ${{ secrets.GH_TOKEN }}
 | 
					          gh_token: ${{ secrets.GH_TOKEN }}
 | 
				
			||||||
          organization: paperless-ngx
 | 
					          organization: paperless-ngx
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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: v3.8.2
 | 
					    rev: v3.8.3
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: reorder-python-imports
 | 
					      - id: reorder-python-imports
 | 
				
			||||||
        exclude: "(migrations)"
 | 
					        exclude: "(migrations)"
 | 
				
			||||||
@@ -59,11 +59,11 @@ 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.6.0
 | 
					    rev: 22.8.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: black
 | 
					      - id: black
 | 
				
			||||||
  - repo: https://github.com/asottile/pyupgrade
 | 
					  - repo: https://github.com/asottile/pyupgrade
 | 
				
			||||||
    rev: v2.37.3
 | 
					    rev: v2.38.1
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: pyupgrade
 | 
					      - id: pyupgrade
 | 
				
			||||||
        exclude: "(migrations)"
 | 
					        exclude: "(migrations)"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -182,7 +182,7 @@ RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \
 | 
				
			|||||||
    --mount=type=bind,from=pikepdf-builder,target=/pikepdf \
 | 
					    --mount=type=bind,from=pikepdf-builder,target=/pikepdf \
 | 
				
			||||||
  set -eux \
 | 
					  set -eux \
 | 
				
			||||||
  && echo "Installing qpdf" \
 | 
					  && echo "Installing qpdf" \
 | 
				
			||||||
    && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf28_*.deb \
 | 
					    && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf29_*.deb \
 | 
				
			||||||
    && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \
 | 
					    && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \
 | 
				
			||||||
  && echo "Installing pikepdf and dependencies" \
 | 
					  && echo "Installing pikepdf and dependencies" \
 | 
				
			||||||
    && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pyparsing*.whl \
 | 
					    && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pyparsing*.whl \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Pipfile
									
									
									
									
									
								
							@@ -23,7 +23,7 @@ imap-tools = "*"
 | 
				
			|||||||
langdetect = "*"
 | 
					langdetect = "*"
 | 
				
			||||||
pathvalidate = "*"
 | 
					pathvalidate = "*"
 | 
				
			||||||
pillow = "~=9.2"
 | 
					pillow = "~=9.2"
 | 
				
			||||||
pikepdf = "~=5.6"
 | 
					pikepdf = "*"
 | 
				
			||||||
python-gnupg = "*"
 | 
					python-gnupg = "*"
 | 
				
			||||||
python-dotenv = "*"
 | 
					python-dotenv = "*"
 | 
				
			||||||
python-dateutil = "*"
 | 
					python-dateutil = "*"
 | 
				
			||||||
@@ -39,7 +39,7 @@ whitenoise = "~=6.2"
 | 
				
			|||||||
watchdog = "~=2.1"
 | 
					watchdog = "~=2.1"
 | 
				
			||||||
whoosh="~=2.7"
 | 
					whoosh="~=2.7"
 | 
				
			||||||
inotifyrecursive = "~=0.3"
 | 
					inotifyrecursive = "~=0.3"
 | 
				
			||||||
ocrmypdf = "~=13.7"
 | 
					ocrmypdf = "~=14.0"
 | 
				
			||||||
tqdm = "*"
 | 
					tqdm = "*"
 | 
				
			||||||
tika = "*"
 | 
					tika = "*"
 | 
				
			||||||
# TODO: This will sadly also install daphne+dependencies,
 | 
					# TODO: This will sadly also install daphne+dependencies,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										743
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										743
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -60,7 +60,7 @@ RUN set -eux \
 | 
				
			|||||||
    && apt-get update --quiet \
 | 
					    && apt-get update --quiet \
 | 
				
			||||||
    && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
 | 
					    && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
 | 
				
			||||||
  && echo "Installing qpdf" \
 | 
					  && echo "Installing qpdf" \
 | 
				
			||||||
    && dpkg --install libqpdf28_*.deb \
 | 
					    && dpkg --install libqpdf29_*.deb \
 | 
				
			||||||
    && dpkg --install libqpdf-dev_*.deb \
 | 
					    && dpkg --install libqpdf-dev_*.deb \
 | 
				
			||||||
  && echo "Installing Python tools" \
 | 
					  && echo "Installing Python tools" \
 | 
				
			||||||
    && python3 -m pip install --no-cache-dir --upgrade \
 | 
					    && python3 -m pip install --no-cache-dir --upgrade \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# This Dockerfile compiles the jbig2enc library
 | 
					# This Dockerfile compiles the jbig2enc library
 | 
				
			||||||
# Inputs:
 | 
					# Inputs:
 | 
				
			||||||
#    - QPDF_VERSION - the version of qpdf to build a .deb.
 | 
					#    - QPDF_VERSION - the version of qpdf to build a .deb.
 | 
				
			||||||
#                     Must be preset as a deb-src
 | 
					#                     Must be present as a deb-src in bookworm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM debian:bullseye-slim as main
 | 
					FROM debian:bullseye-slim as main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,27 +22,23 @@ ARG BUILD_PACKAGES="\
 | 
				
			|||||||
  libjpeg62-turbo-dev \
 | 
					  libjpeg62-turbo-dev \
 | 
				
			||||||
  libgnutls28-dev \
 | 
					  libgnutls28-dev \
 | 
				
			||||||
  packaging-dev \
 | 
					  packaging-dev \
 | 
				
			||||||
 | 
					  cmake \
 | 
				
			||||||
  zlib1g-dev"
 | 
					  zlib1g-dev"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /usr/src
 | 
					WORKDIR /usr/src
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# As this is an base image for a multi-stage final image
 | 
					 | 
				
			||||||
# the added size of the install is basically irrelevant
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RUN set -eux \
 | 
					RUN set -eux \
 | 
				
			||||||
  && echo "Installing build tools" \
 | 
					  && echo "Installing build tools" \
 | 
				
			||||||
    && apt-get update --quiet \
 | 
					    && apt-get update --quiet \
 | 
				
			||||||
    && apt-get install --yes --quiet --no-install-recommends $BUILD_PACKAGES \
 | 
					    && apt-get install --yes --quiet --no-install-recommends $BUILD_PACKAGES \
 | 
				
			||||||
  && echo "Building qpdf" \
 | 
					  && echo "Getting qpdf src" \
 | 
				
			||||||
    && echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
 | 
					    && echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
 | 
				
			||||||
    && apt-get update \
 | 
					    && apt-get update \
 | 
				
			||||||
    && mkdir qpdf \
 | 
					    && mkdir qpdf \
 | 
				
			||||||
    && cd qpdf \
 | 
					    && cd qpdf \
 | 
				
			||||||
    && apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm \
 | 
					    && apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm \
 | 
				
			||||||
 | 
					  && echo "Building qpdf" \
 | 
				
			||||||
    && cd qpdf-$QPDF_VERSION \
 | 
					    && cd qpdf-$QPDF_VERSION \
 | 
				
			||||||
    # We don't need to build the tests (also don't run them)
 | 
					 | 
				
			||||||
    && rm -rf libtests \
 | 
					 | 
				
			||||||
    && DEBEMAIL=hello@paperless-ngx.com debchange --bpo \
 | 
					 | 
				
			||||||
    && export DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2" \
 | 
					    && export DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2" \
 | 
				
			||||||
    && dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
 | 
					    && dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
 | 
				
			||||||
    && ls -ahl ../*.deb \
 | 
					    && ls -ahl ../*.deb \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -218,7 +218,8 @@ using the identifier which it has assigned to each document. You will end up get
 | 
				
			|||||||
files like ``0000123.pdf`` in your media directory. This isn't necessarily a bad
 | 
					files like ``0000123.pdf`` in your media directory. This isn't necessarily a bad
 | 
				
			||||||
thing, because you normally don't have to access these files manually. However, if
 | 
					thing, because you normally don't have to access these files manually. However, if
 | 
				
			||||||
you wish to name your files differently, you can do that by adjusting the
 | 
					you wish to name your files differently, you can do that by adjusting the
 | 
				
			||||||
``PAPERLESS_FILENAME_FORMAT`` configuration option.
 | 
					``PAPERLESS_FILENAME_FORMAT`` configuration option. Paperless adds the correct
 | 
				
			||||||
 | 
					file extension e.g. ``.pdf``, ``.jpg`` automatically.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This variable allows you to configure the filename (folders are allowed) using
 | 
					This variable allows you to configure the filename (folders are allowed) using
 | 
				
			||||||
placeholders. For example, configuring this to
 | 
					placeholders. For example, configuring this to
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,204 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## paperless-ngx 1.9.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Bugfix: Fixes missing OCR mode skip_noarchive [@stumpylog](https://github.com/stumpylog) ([#1645](https://github.com/paperless-ngx/paperless-ngx/pull/1645))
 | 
				
			||||||
 | 
					- Fix reset button padding on small screens [@shamoon](https://github.com/shamoon) ([#1646](https://github.com/paperless-ngx/paperless-ngx/pull/1646))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Improve docs re [@janis-ax](https://github.com/janis-ax) ([#1625](https://github.com/paperless-ngx/paperless-ngx/pull/1625))
 | 
				
			||||||
 | 
					- [Documentation] Add v1.9.0 changelog [@github-actions](https://github.com/github-actions) ([#1639](https://github.com/paperless-ngx/paperless-ngx/pull/1639))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### All App Changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Bugfix: Fixes missing OCR mode skip_noarchive [@stumpylog](https://github.com/stumpylog) ([#1645](https://github.com/paperless-ngx/paperless-ngx/pull/1645))
 | 
				
			||||||
 | 
					- Fix reset button padding on small screens [@shamoon](https://github.com/shamoon) ([#1646](https://github.com/paperless-ngx/paperless-ngx/pull/1646))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## paperless-ngx 1.9.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Feature: Faster, less memory barcode handling [@stumpylog](https://github.com/stumpylog) ([#1594](https://github.com/paperless-ngx/paperless-ngx/pull/1594))
 | 
				
			||||||
 | 
					- Feature: Display django-q process names [@stumpylog](https://github.com/stumpylog) ([#1567](https://github.com/paperless-ngx/paperless-ngx/pull/1567))
 | 
				
			||||||
 | 
					- Feature: Add MariaDB support [@bckelly1](https://github.com/bckelly1) ([#543](https://github.com/paperless-ngx/paperless-ngx/pull/543))
 | 
				
			||||||
 | 
					- Feature: Simplify IMAP login for UTF-8 [@stumpylog](https://github.com/stumpylog) ([#1492](https://github.com/paperless-ngx/paperless-ngx/pull/1492))
 | 
				
			||||||
 | 
					- Feature: Even better re-do of OCR [@stumpylog](https://github.com/stumpylog) ([#1451](https://github.com/paperless-ngx/paperless-ngx/pull/1451))
 | 
				
			||||||
 | 
					- Feature: document comments [@tim-vogel](https://github.com/tim-vogel) ([#1375](https://github.com/paperless-ngx/paperless-ngx/pull/1375))
 | 
				
			||||||
 | 
					- Adding date suggestions to the documents details view [@Eckii24](https://github.com/Eckii24) ([#1367](https://github.com/paperless-ngx/paperless-ngx/pull/1367))
 | 
				
			||||||
 | 
					- Feature: Event driven consumer [@stumpylog](https://github.com/stumpylog) ([#1421](https://github.com/paperless-ngx/paperless-ngx/pull/1421))
 | 
				
			||||||
 | 
					- Feature: Adds storage paths to re-tagger command [@stumpylog](https://github.com/stumpylog) ([#1446](https://github.com/paperless-ngx/paperless-ngx/pull/1446))
 | 
				
			||||||
 | 
					- Feature: Preserve original filename in metadata [@GwynHannay](https://github.com/GwynHannay) ([#1440](https://github.com/paperless-ngx/paperless-ngx/pull/1440))
 | 
				
			||||||
 | 
					- Handle tags for gmail email accounts [@sisao](https://github.com/sisao) ([#1433](https://github.com/paperless-ngx/paperless-ngx/pull/1433))
 | 
				
			||||||
 | 
					- Update redis image [@tribut](https://github.com/tribut) ([#1436](https://github.com/paperless-ngx/paperless-ngx/pull/1436))
 | 
				
			||||||
 | 
					- PAPERLESS_REDIS may be set via docker secrets [@DennisGaida](https://github.com/DennisGaida) ([#1405](https://github.com/paperless-ngx/paperless-ngx/pull/1405))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- paperless_cmd.sh: use exec to run supervisord [@lemmi](https://github.com/lemmi) ([#1617](https://github.com/paperless-ngx/paperless-ngx/pull/1617))
 | 
				
			||||||
 | 
					- Fix: Double barcode separation creates empty file [@stumpylog](https://github.com/stumpylog) ([#1596](https://github.com/paperless-ngx/paperless-ngx/pull/1596))
 | 
				
			||||||
 | 
					- Fix: Resolve issue with slow classifier [@stumpylog](https://github.com/stumpylog) ([#1576](https://github.com/paperless-ngx/paperless-ngx/pull/1576))
 | 
				
			||||||
 | 
					- Fix document comments not updating on document navigation [@shamoon](https://github.com/shamoon) ([#1566](https://github.com/paperless-ngx/paperless-ngx/pull/1566))
 | 
				
			||||||
 | 
					- Fix: Include storage paths in document exporter [@shamoon](https://github.com/shamoon) ([#1557](https://github.com/paperless-ngx/paperless-ngx/pull/1557))
 | 
				
			||||||
 | 
					- Chore: Cleanup and validate settings [@stumpylog](https://github.com/stumpylog) ([#1551](https://github.com/paperless-ngx/paperless-ngx/pull/1551))
 | 
				
			||||||
 | 
					- Bugfix: Better gunicorn settings for workers [@stumpylog](https://github.com/stumpylog) ([#1500](https://github.com/paperless-ngx/paperless-ngx/pull/1500))
 | 
				
			||||||
 | 
					- Fix actions button in tasks table [@shamoon](https://github.com/shamoon) ([#1488](https://github.com/paperless-ngx/paperless-ngx/pull/1488))
 | 
				
			||||||
 | 
					- Fix: Add missing filter rule types to SavedViewFilterRule model \& fix migrations [@shamoon](https://github.com/shamoon) ([#1463](https://github.com/paperless-ngx/paperless-ngx/pull/1463))
 | 
				
			||||||
 | 
					- Fix paperless.conf.example typo [@qcasey](https://github.com/qcasey) ([#1460](https://github.com/paperless-ngx/paperless-ngx/pull/1460))
 | 
				
			||||||
 | 
					- Bugfix: Fixes the creation of an archive file, even if noarchive was specified [@stumpylog](https://github.com/stumpylog) ([#1442](https://github.com/paperless-ngx/paperless-ngx/pull/1442))
 | 
				
			||||||
 | 
					- Fix: created_date should not be required [@shamoon](https://github.com/shamoon) ([#1412](https://github.com/paperless-ngx/paperless-ngx/pull/1412))
 | 
				
			||||||
 | 
					- Fix: dev backend testing [@stumpylog](https://github.com/stumpylog) ([#1420](https://github.com/paperless-ngx/paperless-ngx/pull/1420))
 | 
				
			||||||
 | 
					- Bugfix: Catch all exceptions during the task signals [@stumpylog](https://github.com/stumpylog) ([#1387](https://github.com/paperless-ngx/paperless-ngx/pull/1387))
 | 
				
			||||||
 | 
					- Fix: saved view page parameter [@shamoon](https://github.com/shamoon) ([#1376](https://github.com/paperless-ngx/paperless-ngx/pull/1376))
 | 
				
			||||||
 | 
					- Fix: Correct browser unsaved changes warning [@shamoon](https://github.com/shamoon) ([#1369](https://github.com/paperless-ngx/paperless-ngx/pull/1369))
 | 
				
			||||||
 | 
					- Fix: correct date pasting with other formats [@shamoon](https://github.com/shamoon) ([#1370](https://github.com/paperless-ngx/paperless-ngx/pull/1370))
 | 
				
			||||||
 | 
					- Bugfix: Allow webserver bind address to be configured [@stumpylog](https://github.com/stumpylog) ([#1358](https://github.com/paperless-ngx/paperless-ngx/pull/1358))
 | 
				
			||||||
 | 
					- Bugfix: Chain exceptions during exception handling [@stumpylog](https://github.com/stumpylog) ([#1354](https://github.com/paperless-ngx/paperless-ngx/pull/1354))
 | 
				
			||||||
 | 
					- Fix: missing tooltip translation \& filter editor wrapping [@shamoon](https://github.com/shamoon) ([#1305](https://github.com/paperless-ngx/paperless-ngx/pull/1305))
 | 
				
			||||||
 | 
					- Bugfix: Interaction between barcode and directories as tags [@stumpylog](https://github.com/stumpylog) ([#1303](https://github.com/paperless-ngx/paperless-ngx/pull/1303))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Beta] Paperless-ngx v1.9.0 Release Candidate [@stumpylog](https://github.com/stumpylog) ([#1560](https://github.com/paperless-ngx/paperless-ngx/pull/1560))
 | 
				
			||||||
 | 
					- docs/configuration: Fix binary variable defaults [@erikarvstedt](https://github.com/erikarvstedt) ([#1528](https://github.com/paperless-ngx/paperless-ngx/pull/1528))
 | 
				
			||||||
 | 
					- Info about installing on subpath [@viktor-c](https://github.com/viktor-c) ([#1350](https://github.com/paperless-ngx/paperless-ngx/pull/1350))
 | 
				
			||||||
 | 
					- Docs: move scanner \& software recs to GH wiki [@shamoon](https://github.com/shamoon) ([#1482](https://github.com/paperless-ngx/paperless-ngx/pull/1482))
 | 
				
			||||||
 | 
					- Docs: Update mobile scanner section [@tooomm](https://github.com/tooomm) ([#1467](https://github.com/paperless-ngx/paperless-ngx/pull/1467))
 | 
				
			||||||
 | 
					- Adding date suggestions to the documents details view [@Eckii24](https://github.com/Eckii24) ([#1367](https://github.com/paperless-ngx/paperless-ngx/pull/1367))
 | 
				
			||||||
 | 
					- docs: scanners: add Brother ads4700w [@ocelotsloth](https://github.com/ocelotsloth) ([#1450](https://github.com/paperless-ngx/paperless-ngx/pull/1450))
 | 
				
			||||||
 | 
					- Feature: Adds storage paths to re-tagger command [@stumpylog](https://github.com/stumpylog) ([#1446](https://github.com/paperless-ngx/paperless-ngx/pull/1446))
 | 
				
			||||||
 | 
					- Changes to Redis documentation [@Zerteax](https://github.com/Zerteax) ([#1441](https://github.com/paperless-ngx/paperless-ngx/pull/1441))
 | 
				
			||||||
 | 
					- Update scanners.rst [@glassbox-sco](https://github.com/glassbox-sco) ([#1430](https://github.com/paperless-ngx/paperless-ngx/pull/1430))
 | 
				
			||||||
 | 
					- Update scanners.rst [@derlucas](https://github.com/derlucas) ([#1415](https://github.com/paperless-ngx/paperless-ngx/pull/1415))
 | 
				
			||||||
 | 
					- Bugfix: Allow webserver bind address to be configured [@stumpylog](https://github.com/stumpylog) ([#1358](https://github.com/paperless-ngx/paperless-ngx/pull/1358))
 | 
				
			||||||
 | 
					- docs: fix small typo [@tooomm](https://github.com/tooomm) ([#1352](https://github.com/paperless-ngx/paperless-ngx/pull/1352))
 | 
				
			||||||
 | 
					- [Documentation] Add v1.8.0 changelog [@github-actions](https://github.com/github-actions) ([#1298](https://github.com/paperless-ngx/paperless-ngx/pull/1298))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Maintenance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Beta] Paperless-ngx v1.9.0 Release Candidate [@stumpylog](https://github.com/stumpylog) ([#1560](https://github.com/paperless-ngx/paperless-ngx/pull/1560))
 | 
				
			||||||
 | 
					- paperless_cmd.sh: use exec to run supervisord [@lemmi](https://github.com/lemmi) ([#1617](https://github.com/paperless-ngx/paperless-ngx/pull/1617))
 | 
				
			||||||
 | 
					- Chore: Extended container image cleanup [@stumpylog](https://github.com/stumpylog) ([#1556](https://github.com/paperless-ngx/paperless-ngx/pull/1556))
 | 
				
			||||||
 | 
					- Chore: Smaller library images [@stumpylog](https://github.com/stumpylog) ([#1546](https://github.com/paperless-ngx/paperless-ngx/pull/1546))
 | 
				
			||||||
 | 
					- Bump tj-actions/changed-files from 24 to 29.0.2 [@dependabot](https://github.com/dependabot) ([#1493](https://github.com/paperless-ngx/paperless-ngx/pull/1493))
 | 
				
			||||||
 | 
					- Bugfix: Better gunicorn settings for workers [@stumpylog](https://github.com/stumpylog) ([#1500](https://github.com/paperless-ngx/paperless-ngx/pull/1500))
 | 
				
			||||||
 | 
					- [CI] Fix release drafter issues [@qcasey](https://github.com/qcasey) ([#1301](https://github.com/paperless-ngx/paperless-ngx/pull/1301))
 | 
				
			||||||
 | 
					- Fix: dev backend testing [@stumpylog](https://github.com/stumpylog) ([#1420](https://github.com/paperless-ngx/paperless-ngx/pull/1420))
 | 
				
			||||||
 | 
					- Chore: Exclude dependabot PRs from Project, set status to Needs Review [@qcasey](https://github.com/qcasey) ([#1397](https://github.com/paperless-ngx/paperless-ngx/pull/1397))
 | 
				
			||||||
 | 
					- Chore: Add to label PRs based on and title [@qcasey](https://github.com/qcasey) ([#1396](https://github.com/paperless-ngx/paperless-ngx/pull/1396))
 | 
				
			||||||
 | 
					- Chore: use pre-commit in the Ci workflow [@stumpylog](https://github.com/stumpylog) ([#1362](https://github.com/paperless-ngx/paperless-ngx/pull/1362))
 | 
				
			||||||
 | 
					- Chore: Fixes permissions for image tag cleanup [@stumpylog](https://github.com/stumpylog) ([#1315](https://github.com/paperless-ngx/paperless-ngx/pull/1315))
 | 
				
			||||||
 | 
					- Bump leonsteinhaeuser/project-beta-automations from 1.2.1 to 1.3.0 [@dependabot](https://github.com/dependabot) ([#1328](https://github.com/paperless-ngx/paperless-ngx/pull/1328))
 | 
				
			||||||
 | 
					- Bump tj-actions/changed-files from 23.1 to 24 [@dependabot](https://github.com/dependabot) ([#1329](https://github.com/paperless-ngx/paperless-ngx/pull/1329))
 | 
				
			||||||
 | 
					- Feature: Remove requirements.txt and use pipenv everywhere [@stumpylog](https://github.com/stumpylog) ([#1316](https://github.com/paperless-ngx/paperless-ngx/pull/1316))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<details>
 | 
				
			||||||
 | 
					<summary>34 changes</summary>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Bump pikepdf from 5.5.0 to 5.6.1 [@dependabot](https://github.com/dependabot) ([#1537](https://github.com/paperless-ngx/paperless-ngx/pull/1537))
 | 
				
			||||||
 | 
					- Bump black from 22.6.0 to 22.8.0 [@dependabot](https://github.com/dependabot) ([#1539](https://github.com/paperless-ngx/paperless-ngx/pull/1539))
 | 
				
			||||||
 | 
					- Bump tqdm from 4.64.0 to 4.64.1 [@dependabot](https://github.com/dependabot) ([#1540](https://github.com/paperless-ngx/paperless-ngx/pull/1540))
 | 
				
			||||||
 | 
					- Bump pytest from 7.1.2 to 7.1.3 [@dependabot](https://github.com/dependabot) ([#1538](https://github.com/paperless-ngx/paperless-ngx/pull/1538))
 | 
				
			||||||
 | 
					- Bump tj-actions/changed-files from 24 to 29.0.2 [@dependabot](https://github.com/dependabot) ([#1493](https://github.com/paperless-ngx/paperless-ngx/pull/1493))
 | 
				
			||||||
 | 
					- Bump angular packages, jest-preset-angular in src-ui [@dependabot](https://github.com/dependabot) ([#1502](https://github.com/paperless-ngx/paperless-ngx/pull/1502))
 | 
				
			||||||
 | 
					- Bump jest-environment-jsdom from 28.1.3 to 29.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1507](https://github.com/paperless-ngx/paperless-ngx/pull/1507))
 | 
				
			||||||
 | 
					- Bump [@<!---->types/node from 18.6.3 to 18.7.14 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.6.3 to 18.7.14 in /src-ui @dependabot) ([#1506](https://github.com/paperless-ngx/paperless-ngx/pull/1506))
 | 
				
			||||||
 | 
					- Bump [@<!---->angular-builders/jest from 14.0.0 to 14.0.1 in /src-ui @dependabot](https://github.com/<!---->angular-builders/jest from 14.0.0 to 14.0.1 in /src-ui @dependabot) ([#1505](https://github.com/paperless-ngx/paperless-ngx/pull/1505))
 | 
				
			||||||
 | 
					- Bump zone.js from 0.11.7 to 0.11.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#1504](https://github.com/paperless-ngx/paperless-ngx/pull/1504))
 | 
				
			||||||
 | 
					- Bump ngx-color from 8.0.1 to 8.0.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#1494](https://github.com/paperless-ngx/paperless-ngx/pull/1494))
 | 
				
			||||||
 | 
					- Bump cypress from 10.3.1 to 10.7.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1496](https://github.com/paperless-ngx/paperless-ngx/pull/1496))
 | 
				
			||||||
 | 
					- Bump [@<!---->cypress/schematic from 2.0.0 to 2.1.1 in /src-ui @dependabot](https://github.com/<!---->cypress/schematic from 2.0.0 to 2.1.1 in /src-ui @dependabot) ([#1495](https://github.com/paperless-ngx/paperless-ngx/pull/1495))
 | 
				
			||||||
 | 
					- Bump [@<!---->popperjs/core from 2.11.5 to 2.11.6 in /src-ui @dependabot](https://github.com/<!---->popperjs/core from 2.11.5 to 2.11.6 in /src-ui @dependabot) ([#1498](https://github.com/paperless-ngx/paperless-ngx/pull/1498))
 | 
				
			||||||
 | 
					- Bump sphinx from 5.0.2 to 5.1.1 [@dependabot](https://github.com/dependabot) ([#1297](https://github.com/paperless-ngx/paperless-ngx/pull/1297))
 | 
				
			||||||
 | 
					- Chore: Bump Python dependencies [@stumpylog](https://github.com/stumpylog) ([#1445](https://github.com/paperless-ngx/paperless-ngx/pull/1445))
 | 
				
			||||||
 | 
					- Chore: Update Python deps [@stumpylog](https://github.com/stumpylog) ([#1391](https://github.com/paperless-ngx/paperless-ngx/pull/1391))
 | 
				
			||||||
 | 
					- Bump watchfiles from 0.15.0 to 0.16.1 [@dependabot](https://github.com/dependabot) ([#1285](https://github.com/paperless-ngx/paperless-ngx/pull/1285))
 | 
				
			||||||
 | 
					- Bump leonsteinhaeuser/project-beta-automations from 1.2.1 to 1.3.0 [@dependabot](https://github.com/dependabot) ([#1328](https://github.com/paperless-ngx/paperless-ngx/pull/1328))
 | 
				
			||||||
 | 
					- Bump tj-actions/changed-files from 23.1 to 24 [@dependabot](https://github.com/dependabot) ([#1329](https://github.com/paperless-ngx/paperless-ngx/pull/1329))
 | 
				
			||||||
 | 
					- Bump cypress from 10.3.0 to 10.3.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1342](https://github.com/paperless-ngx/paperless-ngx/pull/1342))
 | 
				
			||||||
 | 
					- Bump ngx-color from 7.3.3 to 8.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1343](https://github.com/paperless-ngx/paperless-ngx/pull/1343))
 | 
				
			||||||
 | 
					- Bump [@<!---->angular/cli from 14.0.4 to 14.1.0 in /src-ui @dependabot](https://github.com/<!---->angular/cli from 14.0.4 to 14.1.0 in /src-ui @dependabot) ([#1330](https://github.com/paperless-ngx/paperless-ngx/pull/1330))
 | 
				
			||||||
 | 
					- Bump [@<!---->types/node from 18.0.0 to 18.6.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.0.0 to 18.6.3 in /src-ui @dependabot) ([#1341](https://github.com/paperless-ngx/paperless-ngx/pull/1341))
 | 
				
			||||||
 | 
					- Bump jest-preset-angular from 12.1.0 to 12.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1340](https://github.com/paperless-ngx/paperless-ngx/pull/1340))
 | 
				
			||||||
 | 
					- Bump concurrently from 7.2.2 to 7.3.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1326](https://github.com/paperless-ngx/paperless-ngx/pull/1326))
 | 
				
			||||||
 | 
					- Bump ng2-pdf-viewer from 9.0.0 to 9.1.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1337](https://github.com/paperless-ngx/paperless-ngx/pull/1337))
 | 
				
			||||||
 | 
					- Bump jest-environment-jsdom from 28.1.2 to 28.1.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#1336](https://github.com/paperless-ngx/paperless-ngx/pull/1336))
 | 
				
			||||||
 | 
					- Bump ngx-file-drop from 13.0.0 to 14.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1331](https://github.com/paperless-ngx/paperless-ngx/pull/1331))
 | 
				
			||||||
 | 
					- Bump jest and [@<!---->types/jest in /src-ui @dependabot](https://github.com/<!---->types/jest in /src-ui @dependabot) ([#1333](https://github.com/paperless-ngx/paperless-ngx/pull/1333))
 | 
				
			||||||
 | 
					- Bump bootstrap from 5.1.3 to 5.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1327](https://github.com/paperless-ngx/paperless-ngx/pull/1327))
 | 
				
			||||||
 | 
					- Bump typescript from 4.6.4 to 4.7.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#1324](https://github.com/paperless-ngx/paperless-ngx/pull/1324))
 | 
				
			||||||
 | 
					- Bump ts-node from 10.8.1 to 10.9.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1325](https://github.com/paperless-ngx/paperless-ngx/pull/1325))
 | 
				
			||||||
 | 
					- Bump rxjs from 7.5.5 to 7.5.6 in /src-ui [@dependabot](https://github.com/dependabot) ([#1323](https://github.com/paperless-ngx/paperless-ngx/pull/1323))
 | 
				
			||||||
 | 
					</details>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### All App Changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Beta] Paperless-ngx v1.9.0 Release Candidate [@stumpylog](https://github.com/stumpylog) ([#1560](https://github.com/paperless-ngx/paperless-ngx/pull/1560))
 | 
				
			||||||
 | 
					- Feature: Faster, less memory barcode handling [@stumpylog](https://github.com/stumpylog) ([#1594](https://github.com/paperless-ngx/paperless-ngx/pull/1594))
 | 
				
			||||||
 | 
					- Fix: Consume directory permissions were not updated [@stumpylog](https://github.com/stumpylog) ([#1605](https://github.com/paperless-ngx/paperless-ngx/pull/1605))
 | 
				
			||||||
 | 
					- Fix: Double barcode separation creates empty file [@stumpylog](https://github.com/stumpylog) ([#1596](https://github.com/paperless-ngx/paperless-ngx/pull/1596))
 | 
				
			||||||
 | 
					- Fix: Parsing Tika documents fails with AttributeError [@stumpylog](https://github.com/stumpylog) ([#1591](https://github.com/paperless-ngx/paperless-ngx/pull/1591))
 | 
				
			||||||
 | 
					- Fix: Resolve issue with slow classifier [@stumpylog](https://github.com/stumpylog) ([#1576](https://github.com/paperless-ngx/paperless-ngx/pull/1576))
 | 
				
			||||||
 | 
					- Feature: Display django-q process names [@stumpylog](https://github.com/stumpylog) ([#1567](https://github.com/paperless-ngx/paperless-ngx/pull/1567))
 | 
				
			||||||
 | 
					- Fix document comments not updating on document navigation [@shamoon](https://github.com/shamoon) ([#1566](https://github.com/paperless-ngx/paperless-ngx/pull/1566))
 | 
				
			||||||
 | 
					- Feature: Add MariaDB support [@bckelly1](https://github.com/bckelly1) ([#543](https://github.com/paperless-ngx/paperless-ngx/pull/543))
 | 
				
			||||||
 | 
					- Fix: Include storage paths in document exporter [@shamoon](https://github.com/shamoon) ([#1557](https://github.com/paperless-ngx/paperless-ngx/pull/1557))
 | 
				
			||||||
 | 
					- Chore: Cleanup and validate settings [@stumpylog](https://github.com/stumpylog) ([#1551](https://github.com/paperless-ngx/paperless-ngx/pull/1551))
 | 
				
			||||||
 | 
					- Bump pikepdf from 5.5.0 to 5.6.1 [@dependabot](https://github.com/dependabot) ([#1537](https://github.com/paperless-ngx/paperless-ngx/pull/1537))
 | 
				
			||||||
 | 
					- Bump black from 22.6.0 to 22.8.0 [@dependabot](https://github.com/dependabot) ([#1539](https://github.com/paperless-ngx/paperless-ngx/pull/1539))
 | 
				
			||||||
 | 
					- Bump tqdm from 4.64.0 to 4.64.1 [@dependabot](https://github.com/dependabot) ([#1540](https://github.com/paperless-ngx/paperless-ngx/pull/1540))
 | 
				
			||||||
 | 
					- Bump pytest from 7.1.2 to 7.1.3 [@dependabot](https://github.com/dependabot) ([#1538](https://github.com/paperless-ngx/paperless-ngx/pull/1538))
 | 
				
			||||||
 | 
					- Bump angular packages, jest-preset-angular in src-ui [@dependabot](https://github.com/dependabot) ([#1502](https://github.com/paperless-ngx/paperless-ngx/pull/1502))
 | 
				
			||||||
 | 
					- Bump jest-environment-jsdom from 28.1.3 to 29.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1507](https://github.com/paperless-ngx/paperless-ngx/pull/1507))
 | 
				
			||||||
 | 
					- Bump [@<!---->types/node from 18.6.3 to 18.7.14 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.6.3 to 18.7.14 in /src-ui @dependabot) ([#1506](https://github.com/paperless-ngx/paperless-ngx/pull/1506))
 | 
				
			||||||
 | 
					- Bump [@<!---->angular-builders/jest from 14.0.0 to 14.0.1 in /src-ui @dependabot](https://github.com/<!---->angular-builders/jest from 14.0.0 to 14.0.1 in /src-ui @dependabot) ([#1505](https://github.com/paperless-ngx/paperless-ngx/pull/1505))
 | 
				
			||||||
 | 
					- Bump zone.js from 0.11.7 to 0.11.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#1504](https://github.com/paperless-ngx/paperless-ngx/pull/1504))
 | 
				
			||||||
 | 
					- Bump ngx-color from 8.0.1 to 8.0.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#1494](https://github.com/paperless-ngx/paperless-ngx/pull/1494))
 | 
				
			||||||
 | 
					- Bump cypress from 10.3.1 to 10.7.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1496](https://github.com/paperless-ngx/paperless-ngx/pull/1496))
 | 
				
			||||||
 | 
					- Bump [@<!---->cypress/schematic from 2.0.0 to 2.1.1 in /src-ui @dependabot](https://github.com/<!---->cypress/schematic from 2.0.0 to 2.1.1 in /src-ui @dependabot) ([#1495](https://github.com/paperless-ngx/paperless-ngx/pull/1495))
 | 
				
			||||||
 | 
					- Bump [@<!---->popperjs/core from 2.11.5 to 2.11.6 in /src-ui @dependabot](https://github.com/<!---->popperjs/core from 2.11.5 to 2.11.6 in /src-ui @dependabot) ([#1498](https://github.com/paperless-ngx/paperless-ngx/pull/1498))
 | 
				
			||||||
 | 
					- Feature: Simplify IMAP login for UTF-8 [@stumpylog](https://github.com/stumpylog) ([#1492](https://github.com/paperless-ngx/paperless-ngx/pull/1492))
 | 
				
			||||||
 | 
					- Fix actions button in tasks table [@shamoon](https://github.com/shamoon) ([#1488](https://github.com/paperless-ngx/paperless-ngx/pull/1488))
 | 
				
			||||||
 | 
					- Fix: Add missing filter rule types to SavedViewFilterRule model \& fix migrations [@shamoon](https://github.com/shamoon) ([#1463](https://github.com/paperless-ngx/paperless-ngx/pull/1463))
 | 
				
			||||||
 | 
					- Feature: Even better re-do of OCR [@stumpylog](https://github.com/stumpylog) ([#1451](https://github.com/paperless-ngx/paperless-ngx/pull/1451))
 | 
				
			||||||
 | 
					- Feature: document comments [@tim-vogel](https://github.com/tim-vogel) ([#1375](https://github.com/paperless-ngx/paperless-ngx/pull/1375))
 | 
				
			||||||
 | 
					- Adding date suggestions to the documents details view [@Eckii24](https://github.com/Eckii24) ([#1367](https://github.com/paperless-ngx/paperless-ngx/pull/1367))
 | 
				
			||||||
 | 
					- Bump sphinx from 5.0.2 to 5.1.1 [@dependabot](https://github.com/dependabot) ([#1297](https://github.com/paperless-ngx/paperless-ngx/pull/1297))
 | 
				
			||||||
 | 
					- Feature: Event driven consumer [@stumpylog](https://github.com/stumpylog) ([#1421](https://github.com/paperless-ngx/paperless-ngx/pull/1421))
 | 
				
			||||||
 | 
					- Bugfix: Fixes the creation of an archive file, even if noarchive was specified [@stumpylog](https://github.com/stumpylog) ([#1442](https://github.com/paperless-ngx/paperless-ngx/pull/1442))
 | 
				
			||||||
 | 
					- Feature: Adds storage paths to re-tagger command [@stumpylog](https://github.com/stumpylog) ([#1446](https://github.com/paperless-ngx/paperless-ngx/pull/1446))
 | 
				
			||||||
 | 
					- Feature: Preserve original filename in metadata [@GwynHannay](https://github.com/GwynHannay) ([#1440](https://github.com/paperless-ngx/paperless-ngx/pull/1440))
 | 
				
			||||||
 | 
					- Handle tags for gmail email accounts [@sisao](https://github.com/sisao) ([#1433](https://github.com/paperless-ngx/paperless-ngx/pull/1433))
 | 
				
			||||||
 | 
					- Fix: should not be required [@shamoon](https://github.com/shamoon) ([#1412](https://github.com/paperless-ngx/paperless-ngx/pull/1412))
 | 
				
			||||||
 | 
					- Bugfix: Catch all exceptions during the task signals [@stumpylog](https://github.com/stumpylog) ([#1387](https://github.com/paperless-ngx/paperless-ngx/pull/1387))
 | 
				
			||||||
 | 
					- Fix: saved view page parameter [@shamoon](https://github.com/shamoon) ([#1376](https://github.com/paperless-ngx/paperless-ngx/pull/1376))
 | 
				
			||||||
 | 
					- Fix: Correct browser unsaved changes warning [@shamoon](https://github.com/shamoon) ([#1369](https://github.com/paperless-ngx/paperless-ngx/pull/1369))
 | 
				
			||||||
 | 
					- Fix: correct date pasting with other formats [@shamoon](https://github.com/shamoon) ([#1370](https://github.com/paperless-ngx/paperless-ngx/pull/1370))
 | 
				
			||||||
 | 
					- Chore: use pre-commit in the Ci workflow [@stumpylog](https://github.com/stumpylog) ([#1362](https://github.com/paperless-ngx/paperless-ngx/pull/1362))
 | 
				
			||||||
 | 
					- Bugfix: Chain exceptions during exception handling [@stumpylog](https://github.com/stumpylog) ([#1354](https://github.com/paperless-ngx/paperless-ngx/pull/1354))
 | 
				
			||||||
 | 
					- Bump watchfiles from 0.15.0 to 0.16.1 [@dependabot](https://github.com/dependabot) ([#1285](https://github.com/paperless-ngx/paperless-ngx/pull/1285))
 | 
				
			||||||
 | 
					- Bump cypress from 10.3.0 to 10.3.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1342](https://github.com/paperless-ngx/paperless-ngx/pull/1342))
 | 
				
			||||||
 | 
					- Bump ngx-color from 7.3.3 to 8.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1343](https://github.com/paperless-ngx/paperless-ngx/pull/1343))
 | 
				
			||||||
 | 
					- Bump [@<!---->angular/cli from 14.0.4 to 14.1.0 in /src-ui @dependabot](https://github.com/<!---->angular/cli from 14.0.4 to 14.1.0 in /src-ui @dependabot) ([#1330](https://github.com/paperless-ngx/paperless-ngx/pull/1330))
 | 
				
			||||||
 | 
					- Bump [@<!---->types/node from 18.0.0 to 18.6.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.0.0 to 18.6.3 in /src-ui @dependabot) ([#1341](https://github.com/paperless-ngx/paperless-ngx/pull/1341))
 | 
				
			||||||
 | 
					- Bump jest-preset-angular from 12.1.0 to 12.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1340](https://github.com/paperless-ngx/paperless-ngx/pull/1340))
 | 
				
			||||||
 | 
					- Bump concurrently from 7.2.2 to 7.3.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1326](https://github.com/paperless-ngx/paperless-ngx/pull/1326))
 | 
				
			||||||
 | 
					- Bump ng2-pdf-viewer from 9.0.0 to 9.1.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1337](https://github.com/paperless-ngx/paperless-ngx/pull/1337))
 | 
				
			||||||
 | 
					- Bump jest-environment-jsdom from 28.1.2 to 28.1.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#1336](https://github.com/paperless-ngx/paperless-ngx/pull/1336))
 | 
				
			||||||
 | 
					- Bump ngx-file-drop from 13.0.0 to 14.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1331](https://github.com/paperless-ngx/paperless-ngx/pull/1331))
 | 
				
			||||||
 | 
					- Bump jest and [@<!---->types/jest in /src-ui @dependabot](https://github.com/<!---->types/jest in /src-ui @dependabot) ([#1333](https://github.com/paperless-ngx/paperless-ngx/pull/1333))
 | 
				
			||||||
 | 
					- Bump bootstrap from 5.1.3 to 5.2.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#1327](https://github.com/paperless-ngx/paperless-ngx/pull/1327))
 | 
				
			||||||
 | 
					- Bump typescript from 4.6.4 to 4.7.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#1324](https://github.com/paperless-ngx/paperless-ngx/pull/1324))
 | 
				
			||||||
 | 
					- Bump ts-node from 10.8.1 to 10.9.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#1325](https://github.com/paperless-ngx/paperless-ngx/pull/1325))
 | 
				
			||||||
 | 
					- Bump rxjs from 7.5.5 to 7.5.6 in /src-ui [@dependabot](https://github.com/dependabot) ([#1323](https://github.com/paperless-ngx/paperless-ngx/pull/1323))
 | 
				
			||||||
 | 
					- Fix: missing tooltip translation \& filter editor wrapping [@shamoon](https://github.com/shamoon) ([#1305](https://github.com/paperless-ngx/paperless-ngx/pull/1305))
 | 
				
			||||||
 | 
					- Feature: Remove requirements.txt and use pipenv everywhere [@stumpylog](https://github.com/stumpylog) ([#1316](https://github.com/paperless-ngx/paperless-ngx/pull/1316))
 | 
				
			||||||
 | 
					- Bugfix: Interaction between barcode and directories as tags [@stumpylog](https://github.com/stumpylog) ([#1303](https://github.com/paperless-ngx/paperless-ngx/pull/1303))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## paperless-ngx 1.8.0
 | 
					## paperless-ngx 1.8.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Features
 | 
					### Features
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -908,18 +908,9 @@ Update Checking
 | 
				
			|||||||
###############
 | 
					###############
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PAPERLESS_ENABLE_UPDATE_CHECK=<bool>
 | 
					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.
 | 
					    .. note::
 | 
				
			||||||
    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.
 | 
					            This setting was deprecated in favor of a frontend setting after v1.9.2. A one-time
 | 
				
			||||||
 | 
					            migration is performed for users who have this setting set. This setting is always
 | 
				
			||||||
    Note that for users of thirdy-party containers e.g. linuxserver.io this notification
 | 
					            ignored if the corresponding frontend setting has been set.
 | 
				
			||||||
    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.
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
myst-parser==0.17.2
 | 
					myst-parser==0.18.1
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										743
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										743
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -13,48 +13,48 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@angular/common": "~14.2.0",
 | 
					    "@angular/common": "~14.2.4",
 | 
				
			||||||
    "@angular/compiler": "~14.2.0",
 | 
					    "@angular/compiler": "~14.2.4",
 | 
				
			||||||
    "@angular/core": "~14.2.0",
 | 
					    "@angular/core": "~14.2.4",
 | 
				
			||||||
    "@angular/forms": "~14.2.0",
 | 
					    "@angular/forms": "~14.2.4",
 | 
				
			||||||
    "@angular/localize": "~14.2.0",
 | 
					    "@angular/localize": "~14.2.4",
 | 
				
			||||||
    "@angular/platform-browser": "~14.2.0",
 | 
					    "@angular/platform-browser": "~14.2.4",
 | 
				
			||||||
    "@angular/platform-browser-dynamic": "~14.2.0",
 | 
					    "@angular/platform-browser-dynamic": "~14.2.4",
 | 
				
			||||||
    "@angular/router": "~14.2.0",
 | 
					    "@angular/router": "~14.2.4",
 | 
				
			||||||
    "@ng-bootstrap/ng-bootstrap": "^13.0.0",
 | 
					    "@ng-bootstrap/ng-bootstrap": "^13.0.0",
 | 
				
			||||||
    "@ng-select/ng-select": "^9.0.2",
 | 
					    "@ng-select/ng-select": "^9.0.2",
 | 
				
			||||||
    "@ngneat/dirty-check-forms": "^3.0.2",
 | 
					    "@ngneat/dirty-check-forms": "^3.0.2",
 | 
				
			||||||
    "@popperjs/core": "^2.11.6",
 | 
					    "@popperjs/core": "^2.11.6",
 | 
				
			||||||
    "bootstrap": "^5.2.0",
 | 
					    "bootstrap": "^5.2.1",
 | 
				
			||||||
    "file-saver": "^2.0.5",
 | 
					    "file-saver": "^2.0.5",
 | 
				
			||||||
    "ng2-pdf-viewer": "^9.1.0",
 | 
					    "ng2-pdf-viewer": "^9.1.2",
 | 
				
			||||||
    "ngx-color": "^8.0.2",
 | 
					    "ngx-color": "^8.0.3",
 | 
				
			||||||
    "ngx-cookie-service": "^14.0.1",
 | 
					    "ngx-cookie-service": "^14.0.1",
 | 
				
			||||||
    "ngx-file-drop": "^14.0.1",
 | 
					    "ngx-file-drop": "^14.0.1",
 | 
				
			||||||
    "rxjs": "~7.5.6",
 | 
					    "rxjs": "~7.5.7",
 | 
				
			||||||
    "tslib": "^2.3.1",
 | 
					    "tslib": "^2.3.1",
 | 
				
			||||||
    "uuid": "^8.3.1",
 | 
					    "uuid": "^9.0.0",
 | 
				
			||||||
    "zone.js": "~0.11.8"
 | 
					    "zone.js": "~0.11.8"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@angular-builders/jest": "14.0.1",
 | 
					    "@angular-builders/jest": "14.0.1",
 | 
				
			||||||
    "@angular-devkit/build-angular": "~14.2.1",
 | 
					    "@angular-devkit/build-angular": "~14.2.4",
 | 
				
			||||||
    "@angular/cli": "~14.2.1",
 | 
					    "@angular/cli": "~14.2.4",
 | 
				
			||||||
    "@angular/compiler-cli": "~14.2.0",
 | 
					    "@angular/compiler-cli": "~14.2.4",
 | 
				
			||||||
    "@types/jest": "28.1.6",
 | 
					    "@types/jest": "28.1.6",
 | 
				
			||||||
    "@types/node": "^18.7.14",
 | 
					    "@types/node": "^18.7.23",
 | 
				
			||||||
    "codelyzer": "^6.0.2",
 | 
					    "codelyzer": "^6.0.2",
 | 
				
			||||||
    "concurrently": "7.3.0",
 | 
					    "concurrently": "7.4.0",
 | 
				
			||||||
    "jest": "28.1.3",
 | 
					    "jest": "28.1.3",
 | 
				
			||||||
    "jest-environment-jsdom": "^29.0.1",
 | 
					    "jest-environment-jsdom": "^29.1.2",
 | 
				
			||||||
    "jest-preset-angular": "^12.2.2",
 | 
					    "jest-preset-angular": "^12.2.2",
 | 
				
			||||||
    "ts-node": "~10.9.1",
 | 
					    "ts-node": "~10.9.1",
 | 
				
			||||||
    "tslint": "~6.1.3",
 | 
					    "tslint": "~6.1.3",
 | 
				
			||||||
    "typescript": "~4.7.4",
 | 
					    "typescript": "~4.8.4",
 | 
				
			||||||
    "wait-on": "~6.0.1"
 | 
					    "wait-on": "~6.0.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "optionalDependencies": {
 | 
					  "optionalDependencies": {
 | 
				
			||||||
    "@cypress/schematic": "^2.1.1",
 | 
					    "@cypress/schematic": "^2.1.1",
 | 
				
			||||||
    "cypress": "~10.7.0"
 | 
					    "cypress": "~10.9.0"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -207,14 +207,25 @@
 | 
				
			|||||||
          <li class="nav-item mt-2" [class.visually-hidden]="slimSidebarEnabled">
 | 
					          <li class="nav-item mt-2" [class.visually-hidden]="slimSidebarEnabled">
 | 
				
			||||||
            <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap">
 | 
					            <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap">
 | 
				
			||||||
              <div class="me-3">{{ versionString }}</div>
 | 
					              <div class="me-3">{{ versionString }}</div>
 | 
				
			||||||
              <div *ngIf="appRemoteVersion" class="version-check">
 | 
					              <div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check">
 | 
				
			||||||
                <ng-template #updateAvailablePopContent>
 | 
					                <ng-template #updateAvailablePopContent>
 | 
				
			||||||
                  <span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
 | 
					                  <span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
 | 
				
			||||||
                </ng-template>
 | 
					                </ng-template>
 | 
				
			||||||
                <ng-template #updateCheckingNotEnabledPopContent>
 | 
					                <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>
 | 
					                  <p class="small mb-2">
 | 
				
			||||||
 | 
					                    <ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
 | 
				
			||||||
 | 
					                  </p>
 | 
				
			||||||
 | 
					                  <div class="btn-group btn-group-xs flex-fill w-100">
 | 
				
			||||||
 | 
					                    <button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
 | 
				
			||||||
 | 
					                    <button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <p class="small mb-0 mt-2">
 | 
				
			||||||
 | 
					                    <a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
 | 
				
			||||||
 | 
					                      How does this work?
 | 
				
			||||||
 | 
					                    </a>
 | 
				
			||||||
 | 
					                  </p>
 | 
				
			||||||
                </ng-template>
 | 
					                </ng-template>
 | 
				
			||||||
                <ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet">
 | 
					                <ng-container *ngIf="settingsService.updateCheckingIsSet; 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"
 | 
					                  <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">
 | 
					                  [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">
 | 
					                    <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
 | 
				
			||||||
@@ -224,8 +235,8 @@
 | 
				
			|||||||
                  </a>
 | 
					                  </a>
 | 
				
			||||||
                </ng-container>
 | 
					                </ng-container>
 | 
				
			||||||
                <ng-template #updateCheckNotSet>
 | 
					                <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"
 | 
					                  <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
 | 
				
			||||||
                  [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
 | 
					                  [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
 | 
				
			||||||
                    <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
 | 
					                    <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" />
 | 
					                      <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
 | 
				
			||||||
                    </svg>
 | 
					                    </svg>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { Component, HostListener } from '@angular/core'
 | 
					import { Component, HostListener, OnInit } from '@angular/core'
 | 
				
			||||||
import { FormControl } from '@angular/forms'
 | 
					import { FormControl } from '@angular/forms'
 | 
				
			||||||
import { ActivatedRoute, Router } from '@angular/router'
 | 
					import { ActivatedRoute, Router } from '@angular/router'
 | 
				
			||||||
import { from, Observable } from 'rxjs'
 | 
					import { from, Observable } from 'rxjs'
 | 
				
			||||||
@@ -32,7 +32,7 @@ import { ToastService } from 'src/app/services/toast.service'
 | 
				
			|||||||
  templateUrl: './app-frame.component.html',
 | 
					  templateUrl: './app-frame.component.html',
 | 
				
			||||||
  styleUrls: ['./app-frame.component.scss'],
 | 
					  styleUrls: ['./app-frame.component.scss'],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AppFrameComponent implements ComponentCanDeactivate {
 | 
					export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public router: Router,
 | 
					    public router: Router,
 | 
				
			||||||
    private activatedRoute: ActivatedRoute,
 | 
					    private activatedRoute: ActivatedRoute,
 | 
				
			||||||
@@ -43,14 +43,14 @@ export class AppFrameComponent implements ComponentCanDeactivate {
 | 
				
			|||||||
    private list: DocumentListViewService,
 | 
					    private list: DocumentListViewService,
 | 
				
			||||||
    public settingsService: SettingsService,
 | 
					    public settingsService: SettingsService,
 | 
				
			||||||
    public tasksService: TasksService,
 | 
					    public tasksService: TasksService,
 | 
				
			||||||
    private toastService: ToastService
 | 
					    private readonly toastService: ToastService
 | 
				
			||||||
  ) {
 | 
					  ) {}
 | 
				
			||||||
    this.remoteVersionService
 | 
					
 | 
				
			||||||
      .checkForUpdates()
 | 
					  ngOnInit(): void {
 | 
				
			||||||
      .subscribe((appRemoteVersion: AppRemoteVersion) => {
 | 
					    if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
 | 
				
			||||||
        this.appRemoteVersion = appRemoteVersion
 | 
					      this.checkForUpdates()
 | 
				
			||||||
      })
 | 
					    }
 | 
				
			||||||
    tasksService.reload()
 | 
					    this.tasksService.reload()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  versionString = `${environment.appTitle} ${environment.version}`
 | 
					  versionString = `${environment.appTitle} ${environment.version}`
 | 
				
			||||||
@@ -182,4 +182,30 @@ export class AppFrameComponent implements ComponentCanDeactivate {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private checkForUpdates() {
 | 
				
			||||||
 | 
					    this.remoteVersionService
 | 
				
			||||||
 | 
					      .checkForUpdates()
 | 
				
			||||||
 | 
					      .subscribe((appRemoteVersion: AppRemoteVersion) => {
 | 
				
			||||||
 | 
					        this.appRemoteVersion = appRemoteVersion
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setUpdateChecking(enable: boolean) {
 | 
				
			||||||
 | 
					    this.settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, enable)
 | 
				
			||||||
 | 
					    this.settingsService
 | 
				
			||||||
 | 
					      .storeSettings()
 | 
				
			||||||
 | 
					      .pipe(first())
 | 
				
			||||||
 | 
					      .subscribe({
 | 
				
			||||||
 | 
					        error: (error) => {
 | 
				
			||||||
 | 
					          this.toastService.showError(
 | 
				
			||||||
 | 
					            $localize`An error occurred while saving update checking settings.`
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          console.log(error)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    if (enable) {
 | 
				
			||||||
 | 
					      this.checkForUpdates()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
 | 
				
			|||||||
import { FormControl, FormGroup } from '@angular/forms'
 | 
					import { FormControl, FormGroup } from '@angular/forms'
 | 
				
			||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
					import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
				
			||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
 | 
					import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
 | 
				
			||||||
 | 
					import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
 | 
				
			||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
 | 
					import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
 | 
				
			||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
 | 
					import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
 | 
				
			||||||
import { ToastService } from 'src/app/services/toast.service'
 | 
					import { ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
@@ -31,7 +32,7 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
 | 
				
			|||||||
  getForm(): FormGroup {
 | 
					  getForm(): FormGroup {
 | 
				
			||||||
    return new FormGroup({
 | 
					    return new FormGroup({
 | 
				
			||||||
      name: new FormControl(''),
 | 
					      name: new FormControl(''),
 | 
				
			||||||
      matching_algorithm: new FormControl(1),
 | 
					      matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
 | 
				
			||||||
      match: new FormControl(''),
 | 
					      match: new FormControl(''),
 | 
				
			||||||
      is_insensitive: new FormControl(true),
 | 
					      is_insensitive: new FormControl(true),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
 | 
				
			|||||||
import { FormControl, FormGroup } from '@angular/forms'
 | 
					import { FormControl, FormGroup } from '@angular/forms'
 | 
				
			||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
					import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
				
			||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
 | 
					import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
 | 
				
			||||||
 | 
					import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
 | 
				
			||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
 | 
					import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
 | 
				
			||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
 | 
					import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
 | 
				
			||||||
import { ToastService } from 'src/app/services/toast.service'
 | 
					import { ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
@@ -31,7 +32,7 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
 | 
				
			|||||||
  getForm(): FormGroup {
 | 
					  getForm(): FormGroup {
 | 
				
			||||||
    return new FormGroup({
 | 
					    return new FormGroup({
 | 
				
			||||||
      name: new FormControl(''),
 | 
					      name: new FormControl(''),
 | 
				
			||||||
      matching_algorithm: new FormControl(1),
 | 
					      matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
 | 
				
			||||||
      match: new FormControl(''),
 | 
					      match: new FormControl(''),
 | 
				
			||||||
      is_insensitive: new FormControl(true),
 | 
					      is_insensitive: new FormControl(true),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
 | 
				
			|||||||
import { FormControl, FormGroup } from '@angular/forms'
 | 
					import { FormControl, FormGroup } from '@angular/forms'
 | 
				
			||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
					import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
				
			||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
 | 
					import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
 | 
				
			||||||
 | 
					import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
 | 
				
			||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
 | 
					import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
 | 
				
			||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
 | 
					import { StoragePathService } from 'src/app/services/rest/storage-path.service'
 | 
				
			||||||
import { ToastService } from 'src/app/services/toast.service'
 | 
					import { ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
@@ -42,7 +43,7 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
 | 
				
			|||||||
    return new FormGroup({
 | 
					    return new FormGroup({
 | 
				
			||||||
      name: new FormControl(''),
 | 
					      name: new FormControl(''),
 | 
				
			||||||
      path: new FormControl(''),
 | 
					      path: new FormControl(''),
 | 
				
			||||||
      matching_algorithm: new FormControl(1),
 | 
					      matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
 | 
				
			||||||
      match: new FormControl(''),
 | 
					      match: new FormControl(''),
 | 
				
			||||||
      is_insensitive: new FormControl(true),
 | 
					      is_insensitive: new FormControl(true),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
 | 
				
			|||||||
import { TagService } from 'src/app/services/rest/tag.service'
 | 
					import { TagService } from 'src/app/services/rest/tag.service'
 | 
				
			||||||
import { ToastService } from 'src/app/services/toast.service'
 | 
					import { ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
import { randomColor } from 'src/app/utils/color'
 | 
					import { randomColor } from 'src/app/utils/color'
 | 
				
			||||||
 | 
					import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-tag-edit-dialog',
 | 
					  selector: 'app-tag-edit-dialog',
 | 
				
			||||||
@@ -34,7 +35,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
 | 
				
			|||||||
      name: new FormControl(''),
 | 
					      name: new FormControl(''),
 | 
				
			||||||
      color: new FormControl(randomColor()),
 | 
					      color: new FormControl(randomColor()),
 | 
				
			||||||
      is_inbox_tag: new FormControl(false),
 | 
					      is_inbox_tag: new FormControl(false),
 | 
				
			||||||
      matching_algorithm: new FormControl(1),
 | 
					      matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
 | 
				
			||||||
      match: new FormControl(''),
 | 
					      match: new FormControl(''),
 | 
				
			||||||
      is_insensitive: new FormControl(true),
 | 
					      is_insensitive: new FormControl(true),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,25 +17,6 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-group-xs {
 | 
					 | 
				
			||||||
  > .btn {
 | 
					 | 
				
			||||||
    padding: 0.2rem 0.25rem;
 | 
					 | 
				
			||||||
    font-size: 0.675rem;
 | 
					 | 
				
			||||||
    line-height: 1.2;
 | 
					 | 
				
			||||||
    border-radius: 0.15rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  > .btn:not(:first-child) {
 | 
					 | 
				
			||||||
    border-top-left-radius: 0;
 | 
					 | 
				
			||||||
    border-bottom-left-radius: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  > .btn:not(:last-child) {
 | 
					 | 
				
			||||||
    border-top-right-radius: 0;
 | 
					 | 
				
			||||||
    border-bottom-right-radius: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.btn-group > label.disabled {
 | 
					.btn-group > label.disabled {
 | 
				
			||||||
  filter: brightness(0.5);
 | 
					  filter: brightness(0.5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,9 +64,9 @@
 | 
				
			|||||||
     </div>
 | 
					     </div>
 | 
				
			||||||
   </div>
 | 
					   </div>
 | 
				
			||||||
   <div class="w-100 d-xxl-none"></div>
 | 
					   <div class="w-100 d-xxl-none"></div>
 | 
				
			||||||
   <div class="col col-xl-auto ps-0">
 | 
					   <div class="col col-xl-auto ps-xxl-0">
 | 
				
			||||||
     <button class="btn btn-link btn-sm px-0" [disabled]="!rulesModified" (click)="resetSelected()">
 | 
					     <button class="btn btn-link btn-sm px-0" [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 ms-n1" 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"/>
 | 
				
			||||||
       </svg><ng-container i18n>Reset filters</ng-container>
 | 
					       </svg><ng-container i18n>Reset filters</ng-container>
 | 
				
			||||||
     </button>
 | 
					     </button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -127,6 +127,21 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="row mb-3">
 | 
				
			||||||
 | 
					          <div class="offset-md-3 col">
 | 
				
			||||||
 | 
					            <p i18n>
 | 
				
			||||||
 | 
					              Update checking works by pinging the the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">Github API</a> for the latest release to determine whether a new version is available.<br/>
 | 
				
			||||||
 | 
					              Actual updating of the app must still be performed manually.
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            <p i18n>
 | 
				
			||||||
 | 
					              <em>No tracking data is collected by the app in any way.</em>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            <app-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled" i18n-hint hint="Note that for users of thirdy-party containers e.g. linuxserver.io this notification may be 'ahead' of the current third-party release."></app-input-check>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <h4 class="mt-4" i18n>Bulk editing</h4>
 | 
					        <h4 class="mt-4" i18n>Bulk editing</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="row mb-3">
 | 
					        <div class="row mb-3">
 | 
				
			||||||
@@ -205,5 +220,5 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
 | 
					  <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <button type="submit" class="btn btn-primary" [disabled]="!(isDirty$ | async)" i18n>Save</button>
 | 
					  <button type="submit" class="btn btn-primary mb-2" [disabled]="!(isDirty$ | async)" i18n>Save</button>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,11 @@
 | 
				
			|||||||
import { Component, Inject, LOCALE_ID, OnInit, OnDestroy } from '@angular/core'
 | 
					import {
 | 
				
			||||||
 | 
					  Component,
 | 
				
			||||||
 | 
					  Inject,
 | 
				
			||||||
 | 
					  LOCALE_ID,
 | 
				
			||||||
 | 
					  OnInit,
 | 
				
			||||||
 | 
					  OnDestroy,
 | 
				
			||||||
 | 
					  AfterViewInit,
 | 
				
			||||||
 | 
					} from '@angular/core'
 | 
				
			||||||
import { FormControl, FormGroup } from '@angular/forms'
 | 
					import { FormControl, FormGroup } from '@angular/forms'
 | 
				
			||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
 | 
					import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
 | 
				
			||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
					import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 | 
				
			||||||
@@ -9,8 +16,18 @@ import {
 | 
				
			|||||||
} from 'src/app/services/settings.service'
 | 
					} from 'src/app/services/settings.service'
 | 
				
			||||||
import { Toast, ToastService } from 'src/app/services/toast.service'
 | 
					import { Toast, ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
 | 
					import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
 | 
				
			||||||
import { Observable, Subscription, BehaviorSubject, first } from 'rxjs'
 | 
					import {
 | 
				
			||||||
 | 
					  Observable,
 | 
				
			||||||
 | 
					  Subscription,
 | 
				
			||||||
 | 
					  BehaviorSubject,
 | 
				
			||||||
 | 
					  first,
 | 
				
			||||||
 | 
					  tap,
 | 
				
			||||||
 | 
					  takeUntil,
 | 
				
			||||||
 | 
					  Subject,
 | 
				
			||||||
 | 
					} from 'rxjs'
 | 
				
			||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
 | 
					import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
 | 
				
			||||||
 | 
					import { ActivatedRoute } from '@angular/router'
 | 
				
			||||||
 | 
					import { ViewportScroller } from '@angular/common'
 | 
				
			||||||
import { ForwardRefHandling } from '@angular/compiler'
 | 
					import { ForwardRefHandling } from '@angular/compiler'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
@@ -18,7 +35,9 @@ import { ForwardRefHandling } from '@angular/compiler'
 | 
				
			|||||||
  templateUrl: './settings.component.html',
 | 
					  templateUrl: './settings.component.html',
 | 
				
			||||||
  styleUrls: ['./settings.component.scss'],
 | 
					  styleUrls: ['./settings.component.scss'],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
					export class SettingsComponent
 | 
				
			||||||
 | 
					  implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
  savedViewGroup = new FormGroup({})
 | 
					  savedViewGroup = new FormGroup({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  settingsForm = new FormGroup({
 | 
					  settingsForm = new FormGroup({
 | 
				
			||||||
@@ -40,6 +59,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
    notificationsConsumerFailed: new FormControl(null),
 | 
					    notificationsConsumerFailed: new FormControl(null),
 | 
				
			||||||
    notificationsConsumerSuppressOnDashboard: new FormControl(null),
 | 
					    notificationsConsumerSuppressOnDashboard: new FormControl(null),
 | 
				
			||||||
    commentsEnabled: new FormControl(null),
 | 
					    commentsEnabled: new FormControl(null),
 | 
				
			||||||
 | 
					    updateCheckingEnabled: new FormControl(null),
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  savedViews: PaperlessSavedView[]
 | 
					  savedViews: PaperlessSavedView[]
 | 
				
			||||||
@@ -47,7 +67,9 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
  store: BehaviorSubject<any>
 | 
					  store: BehaviorSubject<any>
 | 
				
			||||||
  storeSub: Subscription
 | 
					  storeSub: Subscription
 | 
				
			||||||
  isDirty$: Observable<boolean>
 | 
					  isDirty$: Observable<boolean>
 | 
				
			||||||
  isDirty: Boolean = false
 | 
					  isDirty: boolean = false
 | 
				
			||||||
 | 
					  unsubscribeNotifier: Subject<any> = new Subject()
 | 
				
			||||||
 | 
					  savePending: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get computedDateLocale(): string {
 | 
					  get computedDateLocale(): string {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
@@ -57,29 +79,28 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get displayLanguageIsDirty(): boolean {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      this.settingsForm.get('displayLanguage').value !=
 | 
					 | 
				
			||||||
      this.store?.getValue()['displayLanguage']
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public savedViewService: SavedViewService,
 | 
					    public savedViewService: SavedViewService,
 | 
				
			||||||
    private documentListViewService: DocumentListViewService,
 | 
					    private documentListViewService: DocumentListViewService,
 | 
				
			||||||
    private toastService: ToastService,
 | 
					    private toastService: ToastService,
 | 
				
			||||||
    private settings: SettingsService,
 | 
					    private settings: SettingsService,
 | 
				
			||||||
    @Inject(LOCALE_ID) public currentLocale: string
 | 
					    @Inject(LOCALE_ID) public currentLocale: string,
 | 
				
			||||||
 | 
					    private viewportScroller: ViewportScroller,
 | 
				
			||||||
 | 
					    private activatedRoute: ActivatedRoute
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.settings.changed.subscribe({
 | 
					    this.settings.settingsSaved.subscribe(() => {
 | 
				
			||||||
      next: () => {
 | 
					      if (!this.savePending) this.initialize()
 | 
				
			||||||
        this.settingsForm.patchValue(this.getCurrentSettings(), {
 | 
					 | 
				
			||||||
          emitEvent: false,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
 | 
					    if (this.activatedRoute.snapshot.fragment) {
 | 
				
			||||||
 | 
					      this.viewportScroller.scrollToAnchor(
 | 
				
			||||||
 | 
					        this.activatedRoute.snapshot.fragment
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private getCurrentSettings() {
 | 
					  private getCurrentSettings() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      bulkEditConfirmationDialogs: this.settings.get(
 | 
					      bulkEditConfirmationDialogs: this.settings.get(
 | 
				
			||||||
@@ -91,7 +112,6 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
      documentListItemPerPage: this.settings.get(
 | 
					      documentListItemPerPage: this.settings.get(
 | 
				
			||||||
        SETTINGS_KEYS.DOCUMENT_LIST_SIZE
 | 
					        SETTINGS_KEYS.DOCUMENT_LIST_SIZE
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      slimSidebarEnabled: this.settings.get(SETTINGS_KEYS.SLIM_SIDEBAR),
 | 
					 | 
				
			||||||
      darkModeUseSystem: this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM),
 | 
					      darkModeUseSystem: this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM),
 | 
				
			||||||
      darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED),
 | 
					      darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED),
 | 
				
			||||||
      darkModeInvertThumbs: this.settings.get(
 | 
					      darkModeInvertThumbs: this.settings.get(
 | 
				
			||||||
@@ -118,55 +138,68 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
        SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
 | 
					        SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
 | 
					      commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
 | 
				
			||||||
 | 
					      updateCheckingEnabled: this.settings.get(
 | 
				
			||||||
 | 
					        SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit() {
 | 
					  ngOnInit() {
 | 
				
			||||||
    this.savedViewService.listAll().subscribe((r) => {
 | 
					    this.savedViewService.listAll().subscribe((r) => {
 | 
				
			||||||
      this.savedViews = r.results
 | 
					      this.savedViews = r.results
 | 
				
			||||||
      let storeData = this.getCurrentSettings()
 | 
					      this.initialize()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (let view of this.savedViews) {
 | 
					  initialize() {
 | 
				
			||||||
        storeData.savedViews[view.id.toString()] = {
 | 
					    this.unsubscribeNotifier.next(true)
 | 
				
			||||||
          id: view.id,
 | 
					
 | 
				
			||||||
          name: view.name,
 | 
					    let storeData = this.getCurrentSettings()
 | 
				
			||||||
          show_on_dashboard: view.show_on_dashboard,
 | 
					
 | 
				
			||||||
          show_in_sidebar: view.show_in_sidebar,
 | 
					    for (let view of this.savedViews) {
 | 
				
			||||||
        }
 | 
					      storeData.savedViews[view.id.toString()] = {
 | 
				
			||||||
        this.savedViewGroup.addControl(
 | 
					        id: view.id,
 | 
				
			||||||
          view.id.toString(),
 | 
					        name: view.name,
 | 
				
			||||||
          new FormGroup({
 | 
					        show_on_dashboard: view.show_on_dashboard,
 | 
				
			||||||
            id: new FormControl(null),
 | 
					        show_in_sidebar: view.show_in_sidebar,
 | 
				
			||||||
            name: new FormControl(null),
 | 
					 | 
				
			||||||
            show_on_dashboard: new FormControl(null),
 | 
					 | 
				
			||||||
            show_in_sidebar: new FormControl(null),
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.savedViewGroup.addControl(
 | 
				
			||||||
 | 
					        view.id.toString(),
 | 
				
			||||||
 | 
					        new FormGroup({
 | 
				
			||||||
 | 
					          id: new FormControl(null),
 | 
				
			||||||
 | 
					          name: new FormControl(null),
 | 
				
			||||||
 | 
					          show_on_dashboard: new FormControl(null),
 | 
				
			||||||
 | 
					          show_in_sidebar: new FormControl(null),
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.store = new BehaviorSubject(storeData)
 | 
					    this.store = new BehaviorSubject(storeData)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.storeSub = this.store.asObservable().subscribe((state) => {
 | 
					    this.storeSub = this.store.asObservable().subscribe((state) => {
 | 
				
			||||||
        this.settingsForm.patchValue(state, { emitEvent: false })
 | 
					      this.settingsForm.patchValue(state, { emitEvent: false })
 | 
				
			||||||
      })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Initialize dirtyCheck
 | 
					    // Initialize dirtyCheck
 | 
				
			||||||
      this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable())
 | 
					    this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Record dirty in case we need to 'undo' appearance settings if not saved on close
 | 
					    // Record dirty in case we need to 'undo' appearance settings if not saved on close
 | 
				
			||||||
      this.isDirty$.subscribe((dirty) => {
 | 
					    this.isDirty$
 | 
				
			||||||
 | 
					      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
				
			||||||
 | 
					      .subscribe((dirty) => {
 | 
				
			||||||
        this.isDirty = dirty
 | 
					        this.isDirty = dirty
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // "Live" visual changes prior to save
 | 
					    // "Live" visual changes prior to save
 | 
				
			||||||
      this.settingsForm.valueChanges.subscribe(() => {
 | 
					    this.settingsForm.valueChanges
 | 
				
			||||||
 | 
					      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
				
			||||||
 | 
					      .subscribe(() => {
 | 
				
			||||||
        this.settings.updateAppearanceSettings(
 | 
					        this.settings.updateAppearanceSettings(
 | 
				
			||||||
          this.settingsForm.get('darkModeUseSystem').value,
 | 
					          this.settingsForm.get('darkModeUseSystem').value,
 | 
				
			||||||
          this.settingsForm.get('darkModeEnabled').value,
 | 
					          this.settingsForm.get('darkModeEnabled').value,
 | 
				
			||||||
          this.settingsForm.get('themeColor').value
 | 
					          this.settingsForm.get('themeColor').value
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnDestroy() {
 | 
					  ngOnDestroy() {
 | 
				
			||||||
@@ -185,7 +218,14 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private saveLocalSettings() {
 | 
					  private saveLocalSettings() {
 | 
				
			||||||
    const reloadRequired = this.displayLanguageIsDirty // just this one, for now
 | 
					    this.savePending = true
 | 
				
			||||||
 | 
					    const reloadRequired =
 | 
				
			||||||
 | 
					      this.settingsForm.value.displayLanguage !=
 | 
				
			||||||
 | 
					        this.store?.getValue()['displayLanguage'] || // displayLanguage is dirty
 | 
				
			||||||
 | 
					      (this.settingsForm.value.updateCheckingEnabled !=
 | 
				
			||||||
 | 
					        this.store?.getValue()['updateCheckingEnabled'] &&
 | 
				
			||||||
 | 
					        this.settingsForm.value.updateCheckingEnabled) // update checking was turned on
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.settings.set(
 | 
					    this.settings.set(
 | 
				
			||||||
      SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE,
 | 
					      SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE,
 | 
				
			||||||
      this.settingsForm.value.bulkEditApplyOnClose
 | 
					      this.settingsForm.value.bulkEditApplyOnClose
 | 
				
			||||||
@@ -250,10 +290,15 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
 | 
				
			|||||||
      SETTINGS_KEYS.COMMENTS_ENABLED,
 | 
					      SETTINGS_KEYS.COMMENTS_ENABLED,
 | 
				
			||||||
      this.settingsForm.value.commentsEnabled
 | 
					      this.settingsForm.value.commentsEnabled
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    this.settings.set(
 | 
				
			||||||
 | 
					      SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
 | 
				
			||||||
 | 
					      this.settingsForm.value.updateCheckingEnabled
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    this.settings.setLanguage(this.settingsForm.value.displayLanguage)
 | 
					    this.settings.setLanguage(this.settingsForm.value.displayLanguage)
 | 
				
			||||||
    this.settings
 | 
					    this.settings
 | 
				
			||||||
      .storeSettings()
 | 
					      .storeSettings()
 | 
				
			||||||
      .pipe(first())
 | 
					      .pipe(first())
 | 
				
			||||||
 | 
					      .pipe(tap(() => (this.savePending = false)))
 | 
				
			||||||
      .subscribe({
 | 
					      .subscribe({
 | 
				
			||||||
        next: () => {
 | 
					        next: () => {
 | 
				
			||||||
          this.store.next(this.settingsForm.value)
 | 
					          this.store.next(this.settingsForm.value)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ export const MATCH_LITERAL = 3
 | 
				
			|||||||
export const MATCH_REGEX = 4
 | 
					export const MATCH_REGEX = 4
 | 
				
			||||||
export const MATCH_FUZZY = 5
 | 
					export const MATCH_FUZZY = 5
 | 
				
			||||||
export const MATCH_AUTO = 6
 | 
					export const MATCH_AUTO = 6
 | 
				
			||||||
 | 
					export const DEFAULT_MATCHING_ALGORITHM = MATCH_AUTO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MATCHING_ALGORITHMS = [
 | 
					export const MATCHING_ALGORITHMS = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,8 +29,6 @@ export interface PaperlessDocument extends ObjectWithId {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  content?: string
 | 
					  content?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  file_type?: string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  tags$?: Observable<PaperlessTag[]>
 | 
					  tags$?: Observable<PaperlessTag[]>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  tags?: number[]
 | 
					  tags?: number[]
 | 
				
			||||||
@@ -47,7 +45,7 @@ export interface PaperlessDocument extends ObjectWithId {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  added?: Date
 | 
					  added?: Date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  file_name?: string
 | 
					  original_file_name?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  download_url?: string
 | 
					  download_url?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,9 @@ export const SETTINGS_KEYS = {
 | 
				
			|||||||
    'general-settings:notifications:consumer-suppress-on-dashboard',
 | 
					    'general-settings:notifications:consumer-suppress-on-dashboard',
 | 
				
			||||||
  COMMENTS_ENABLED: 'general-settings:comments-enabled',
 | 
					  COMMENTS_ENABLED: 'general-settings:comments-enabled',
 | 
				
			||||||
  SLIM_SIDEBAR: 'general-settings:slim-sidebar',
 | 
					  SLIM_SIDEBAR: 'general-settings:slim-sidebar',
 | 
				
			||||||
 | 
					  UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
 | 
				
			||||||
 | 
					  UPDATE_CHECKING_BACKEND_SETTING:
 | 
				
			||||||
 | 
					    'general-settings:update-checking:backend-setting',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SETTINGS: PaperlessUiSetting[] = [
 | 
					export const SETTINGS: PaperlessUiSetting[] = [
 | 
				
			||||||
@@ -126,4 +129,14 @@ export const SETTINGS: PaperlessUiSetting[] = [
 | 
				
			|||||||
    type: 'boolean',
 | 
					    type: 'boolean',
 | 
				
			||||||
    default: true,
 | 
					    default: true,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
 | 
				
			||||||
 | 
					    type: 'boolean',
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    key: SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING,
 | 
				
			||||||
 | 
					    type: 'string',
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ import { environment } from 'src/environments/environment'
 | 
				
			|||||||
export interface AppRemoteVersion {
 | 
					export interface AppRemoteVersion {
 | 
				
			||||||
  version: string
 | 
					  version: string
 | 
				
			||||||
  update_available: boolean
 | 
					  update_available: boolean
 | 
				
			||||||
  feature_is_set: boolean
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable({
 | 
					@Injectable({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,7 @@ export class SettingsService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public displayName: string
 | 
					  public displayName: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public changed = new EventEmitter()
 | 
					  public settingsSaved: EventEmitter<any> = new EventEmitter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    rendererFactory: RendererFactory2,
 | 
					    rendererFactory: RendererFactory2,
 | 
				
			||||||
@@ -316,13 +316,7 @@ export class SettingsService {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get(key: string): any {
 | 
					  private getSettingRawValue(key: string): any {
 | 
				
			||||||
    let setting = SETTINGS.find((s) => s.key == key)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!setting) {
 | 
					 | 
				
			||||||
      return null
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let value = null
 | 
					    let value = null
 | 
				
			||||||
    // parse key:key:key into nested object
 | 
					    // parse key:key:key into nested object
 | 
				
			||||||
    const keys = key.replace('general-settings:', '').split(':')
 | 
					    const keys = key.replace('general-settings:', '').split(':')
 | 
				
			||||||
@@ -333,6 +327,17 @@ export class SettingsService {
 | 
				
			|||||||
      if (index == keys.length - 1) value = settingObj[keyPart]
 | 
					      if (index == keys.length - 1) value = settingObj[keyPart]
 | 
				
			||||||
      else settingObj = settingObj[keyPart]
 | 
					      else settingObj = settingObj[keyPart]
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    return value
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get(key: string): any {
 | 
				
			||||||
 | 
					    let setting = SETTINGS.find((s) => s.key == key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!setting) {
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let value = this.getSettingRawValue(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (value != null) {
 | 
					    if (value != null) {
 | 
				
			||||||
      switch (setting.type) {
 | 
					      switch (setting.type) {
 | 
				
			||||||
@@ -362,10 +367,19 @@ export class SettingsService {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private settingIsSet(key: string): boolean {
 | 
				
			||||||
 | 
					    let value = this.getSettingRawValue(key)
 | 
				
			||||||
 | 
					    return value != null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  storeSettings(): Observable<any> {
 | 
					  storeSettings(): Observable<any> {
 | 
				
			||||||
    return this.http
 | 
					    return this.http.post(this.baseUrl, { settings: this.settings }).pipe(
 | 
				
			||||||
      .post(this.baseUrl, { settings: this.settings })
 | 
					      tap((results) => {
 | 
				
			||||||
      .pipe(tap((result) => this.changed.emit(!!result.success)))
 | 
					        if (results.success) {
 | 
				
			||||||
 | 
					          this.settingsSaved.emit()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  maybeMigrateSettings() {
 | 
					  maybeMigrateSettings() {
 | 
				
			||||||
@@ -405,5 +419,31 @@ export class SettingsService {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) &&
 | 
				
			||||||
 | 
					      this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING) != 'default'
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      this.set(
 | 
				
			||||||
 | 
					        SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
 | 
				
			||||||
 | 
					        this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING).toString() ===
 | 
				
			||||||
 | 
					          'true'
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.storeSettings()
 | 
				
			||||||
 | 
					        .pipe(first())
 | 
				
			||||||
 | 
					        .subscribe({
 | 
				
			||||||
 | 
					          error: (e) => {
 | 
				
			||||||
 | 
					            this.toastService.showError(
 | 
				
			||||||
 | 
					              'Error migrating update checking setting'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            console.log(e)
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get updateCheckingIsSet(): boolean {
 | 
				
			||||||
 | 
					    return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ export const environment = {
 | 
				
			|||||||
  apiBaseUrl: document.baseURI + 'api/',
 | 
					  apiBaseUrl: document.baseURI + 'api/',
 | 
				
			||||||
  apiVersion: '2',
 | 
					  apiVersion: '2',
 | 
				
			||||||
  appTitle: 'Paperless-ngx',
 | 
					  appTitle: 'Paperless-ngx',
 | 
				
			||||||
  version: '1.9.0-dev',
 | 
					  version: '1.9.2-dev',
 | 
				
			||||||
  webSocketHost: window.location.host,
 | 
					  webSocketHost: window.location.host,
 | 
				
			||||||
  webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
 | 
					  webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
 | 
				
			||||||
  webSocketBaseUrl: base_url.pathname + 'ws/',
 | 
					  webSocketBaseUrl: base_url.pathname + 'ws/',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -540,6 +540,25 @@ a.badge {
 | 
				
			|||||||
    border-color: var(--bs-primary);
 | 
					    border-color: var(--bs-primary);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-group-xs {
 | 
				
			||||||
 | 
					  > .btn {
 | 
				
			||||||
 | 
					    padding: 0.2rem 0.25rem;
 | 
				
			||||||
 | 
					    font-size: 0.675rem;
 | 
				
			||||||
 | 
					    line-height: 1.2;
 | 
				
			||||||
 | 
					    border-radius: 0.15rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  > .btn:not(:first-child) {
 | 
				
			||||||
 | 
					    border-top-left-radius: 0;
 | 
				
			||||||
 | 
					    border-bottom-left-radius: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  > .btn:not(:last-child) {
 | 
				
			||||||
 | 
					    border-top-right-radius: 0;
 | 
				
			||||||
 | 
					    border-bottom-right-radius: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
code {
 | 
					code {
 | 
				
			||||||
  color: var(--pngx-body-color-accent)
 | 
					  color: var(--pngx-body-color-accent)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -111,14 +111,16 @@ class Consumer(LoggingMixin):
 | 
				
			|||||||
    def pre_check_duplicate(self):
 | 
					    def pre_check_duplicate(self):
 | 
				
			||||||
        with open(self.path, "rb") as f:
 | 
					        with open(self.path, "rb") as f:
 | 
				
			||||||
            checksum = hashlib.md5(f.read()).hexdigest()
 | 
					            checksum = hashlib.md5(f.read()).hexdigest()
 | 
				
			||||||
        if Document.objects.filter(
 | 
					        existing_doc = Document.objects.filter(
 | 
				
			||||||
            Q(checksum=checksum) | Q(archive_checksum=checksum),
 | 
					            Q(checksum=checksum) | Q(archive_checksum=checksum),
 | 
				
			||||||
        ).exists():
 | 
					        )
 | 
				
			||||||
 | 
					        if existing_doc.exists():
 | 
				
			||||||
            if settings.CONSUMER_DELETE_DUPLICATES:
 | 
					            if settings.CONSUMER_DELETE_DUPLICATES:
 | 
				
			||||||
                os.unlink(self.path)
 | 
					                os.unlink(self.path)
 | 
				
			||||||
            self._fail(
 | 
					            self._fail(
 | 
				
			||||||
                MESSAGE_DOCUMENT_ALREADY_EXISTS,
 | 
					                MESSAGE_DOCUMENT_ALREADY_EXISTS,
 | 
				
			||||||
                f"Not consuming {self.filename}: It is a duplicate.",
 | 
					                f"Not consuming {self.filename}: It is a duplicate of"
 | 
				
			||||||
 | 
					                f" {existing_doc.get().title} (#{existing_doc.get().pk})",
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pre_check_directories(self):
 | 
					    def pre_check_directories(self):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -608,6 +608,15 @@ class UiSettingsViewSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
            "settings",
 | 
					            "settings",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_settings(self, settings):
 | 
				
			||||||
 | 
					        # we never save update checking backend setting
 | 
				
			||||||
 | 
					        if "update_checking" in settings:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                settings["update_checking"].pop("backend_setting")
 | 
				
			||||||
 | 
					            except KeyError:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					        return settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data):
 | 
					    def create(self, validated_data):
 | 
				
			||||||
        ui_settings = UiSettings.objects.update_or_create(
 | 
					        ui_settings = UiSettings.objects.update_or_create(
 | 
				
			||||||
            user=validated_data.get("user"),
 | 
					            user=validated_data.get("user"),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,10 +112,22 @@ def consume_file(
 | 
				
			|||||||
                        newname = f"{str(n)}_" + override_filename
 | 
					                        newname = f"{str(n)}_" + override_filename
 | 
				
			||||||
                    else:
 | 
					                    else:
 | 
				
			||||||
                        newname = None
 | 
					                        newname = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # If the file is an upload, it's in the scratch directory
 | 
				
			||||||
 | 
					                    # Move it to consume directory to be picked up
 | 
				
			||||||
 | 
					                    # Otherwise, use the current parent to keep possible tags
 | 
				
			||||||
 | 
					                    # from subdirectories
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        # is_relative_to would be nicer, but new in 3.9
 | 
				
			||||||
 | 
					                        _ = path.relative_to(settings.SCRATCH_DIR)
 | 
				
			||||||
 | 
					                        save_to_dir = settings.CONSUMPTION_DIR
 | 
				
			||||||
 | 
					                    except ValueError:
 | 
				
			||||||
 | 
					                        save_to_dir = path.parent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    barcodes.save_to_dir(
 | 
					                    barcodes.save_to_dir(
 | 
				
			||||||
                        document,
 | 
					                        document,
 | 
				
			||||||
                        newname=newname,
 | 
					                        newname=newname,
 | 
				
			||||||
                        target_dir=path.parent,
 | 
					                        target_dir=save_to_dir,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # Delete the PDF file which was split
 | 
					                # Delete the PDF file which was split
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1581,7 +1581,11 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        self.assertDictEqual(
 | 
					        self.assertDictEqual(
 | 
				
			||||||
            response.data["settings"],
 | 
					            response.data["settings"],
 | 
				
			||||||
            {},
 | 
					            {
 | 
				
			||||||
 | 
					                "update_checking": {
 | 
				
			||||||
 | 
					                    "backend_setting": "default",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_api_set_ui_settings(self):
 | 
					    def test_api_set_ui_settings(self):
 | 
				
			||||||
@@ -2542,38 +2546,6 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        super().setUp()
 | 
					        super().setUp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_remote_version_default(self):
 | 
					 | 
				
			||||||
        response = self.client.get(self.ENDPOINT)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
        self.assertDictEqual(
 | 
					 | 
				
			||||||
            response.data,
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                "version": "0.0.0",
 | 
					 | 
				
			||||||
                "update_available": False,
 | 
					 | 
				
			||||||
                "feature_is_set": False,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @override_settings(
 | 
					 | 
				
			||||||
        ENABLE_UPDATE_CHECK=False,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def test_remote_version_disabled(self):
 | 
					 | 
				
			||||||
        response = self.client.get(self.ENDPOINT)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					 | 
				
			||||||
        self.assertDictEqual(
 | 
					 | 
				
			||||||
            response.data,
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                "version": "0.0.0",
 | 
					 | 
				
			||||||
                "update_available": False,
 | 
					 | 
				
			||||||
                "feature_is_set": True,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @override_settings(
 | 
					 | 
				
			||||||
        ENABLE_UPDATE_CHECK=True,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @mock.patch("urllib.request.urlopen")
 | 
					    @mock.patch("urllib.request.urlopen")
 | 
				
			||||||
    def test_remote_version_enabled_no_update_prefix(self, urlopen_mock):
 | 
					    def test_remote_version_enabled_no_update_prefix(self, urlopen_mock):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2591,13 +2563,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "version": "1.6.0",
 | 
					                "version": "1.6.0",
 | 
				
			||||||
                "update_available": False,
 | 
					                "update_available": False,
 | 
				
			||||||
                "feature_is_set": True,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					 | 
				
			||||||
        ENABLE_UPDATE_CHECK=True,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @mock.patch("urllib.request.urlopen")
 | 
					    @mock.patch("urllib.request.urlopen")
 | 
				
			||||||
    def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock):
 | 
					    def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2617,13 +2585,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "version": version.__full_version_str__,
 | 
					                "version": version.__full_version_str__,
 | 
				
			||||||
                "update_available": False,
 | 
					                "update_available": False,
 | 
				
			||||||
                "feature_is_set": True,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					 | 
				
			||||||
        ENABLE_UPDATE_CHECK=True,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @mock.patch("urllib.request.urlopen")
 | 
					    @mock.patch("urllib.request.urlopen")
 | 
				
			||||||
    def test_remote_version_enabled_update(self, urlopen_mock):
 | 
					    def test_remote_version_enabled_update(self, urlopen_mock):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2650,13 +2614,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "version": new_version_str,
 | 
					                "version": new_version_str,
 | 
				
			||||||
                "update_available": True,
 | 
					                "update_available": True,
 | 
				
			||||||
                "feature_is_set": True,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					 | 
				
			||||||
        ENABLE_UPDATE_CHECK=True,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @mock.patch("urllib.request.urlopen")
 | 
					    @mock.patch("urllib.request.urlopen")
 | 
				
			||||||
    def test_remote_version_bad_json(self, urlopen_mock):
 | 
					    def test_remote_version_bad_json(self, urlopen_mock):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2674,13 +2634,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "version": "0.0.0",
 | 
					                "version": "0.0.0",
 | 
				
			||||||
                "update_available": False,
 | 
					                "update_available": False,
 | 
				
			||||||
                "feature_is_set": True,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					 | 
				
			||||||
        ENABLE_UPDATE_CHECK=True,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @mock.patch("urllib.request.urlopen")
 | 
					    @mock.patch("urllib.request.urlopen")
 | 
				
			||||||
    def test_remote_version_exception(self, urlopen_mock):
 | 
					    def test_remote_version_exception(self, urlopen_mock):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2698,7 +2654,6 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "version": "0.0.0",
 | 
					                "version": "0.0.0",
 | 
				
			||||||
                "update_available": False,
 | 
					                "update_available": False,
 | 
				
			||||||
                "feature_is_set": True,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -261,6 +261,9 @@ class DocumentViewSet(
 | 
				
			|||||||
            file_handle = doc.source_file
 | 
					            file_handle = doc.source_file
 | 
				
			||||||
            filename = doc.get_public_filename()
 | 
					            filename = doc.get_public_filename()
 | 
				
			||||||
            mime_type = doc.mime_type
 | 
					            mime_type = doc.mime_type
 | 
				
			||||||
 | 
					            # Support browser previewing csv files by using text mime type
 | 
				
			||||||
 | 
					            if mime_type in {"application/csv", "text/csv"} and disposition == "inline":
 | 
				
			||||||
 | 
					                mime_type = "text/plain"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if doc.storage_type == Document.STORAGE_TYPE_GPG:
 | 
					        if doc.storage_type == Document.STORAGE_TYPE_GPG:
 | 
				
			||||||
            file_handle = GnuPG.decrypted(file_handle)
 | 
					            file_handle = GnuPG.decrypted(file_handle)
 | 
				
			||||||
@@ -780,42 +783,38 @@ class RemoteVersionView(GenericAPIView):
 | 
				
			|||||||
        remote_version = "0.0.0"
 | 
					        remote_version = "0.0.0"
 | 
				
			||||||
        is_greater_than_current = False
 | 
					        is_greater_than_current = False
 | 
				
			||||||
        current_version = packaging_version.parse(version.__full_version_str__)
 | 
					        current_version = packaging_version.parse(version.__full_version_str__)
 | 
				
			||||||
        # TODO: this can likely be removed when frontend settings are saved to DB
 | 
					        try:
 | 
				
			||||||
        feature_is_set = settings.ENABLE_UPDATE_CHECK != "default"
 | 
					            req = urllib.request.Request(
 | 
				
			||||||
        if feature_is_set and settings.ENABLE_UPDATE_CHECK:
 | 
					                "https://api.github.com/repos/paperless-ngx/"
 | 
				
			||||||
            try:
 | 
					                "paperless-ngx/releases/latest",
 | 
				
			||||||
                req = urllib.request.Request(
 | 
					 | 
				
			||||||
                    "https://api.github.com/repos/paperless-ngx/"
 | 
					 | 
				
			||||||
                    "paperless-ngx/releases/latest",
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                # Ensure a JSON response
 | 
					 | 
				
			||||||
                req.add_header("Accept", "application/json")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                with urllib.request.urlopen(req) as response:
 | 
					 | 
				
			||||||
                    remote = response.read().decode("utf-8")
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    remote_json = json.loads(remote)
 | 
					 | 
				
			||||||
                    remote_version = remote_json["tag_name"]
 | 
					 | 
				
			||||||
                    # Basically PEP 616 but that only went in 3.9
 | 
					 | 
				
			||||||
                    if remote_version.startswith("ngx-"):
 | 
					 | 
				
			||||||
                        remote_version = remote_version[len("ngx-") :]
 | 
					 | 
				
			||||||
                except ValueError:
 | 
					 | 
				
			||||||
                    logger.debug("An error occurred parsing remote version json")
 | 
					 | 
				
			||||||
            except urllib.error.URLError:
 | 
					 | 
				
			||||||
                logger.debug("An error occurred checking for available updates")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            is_greater_than_current = (
 | 
					 | 
				
			||||||
                packaging_version.parse(
 | 
					 | 
				
			||||||
                    remote_version,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                > current_version
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            # Ensure a JSON response
 | 
				
			||||||
 | 
					            req.add_header("Accept", "application/json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            with urllib.request.urlopen(req) as response:
 | 
				
			||||||
 | 
					                remote = response.read().decode("utf-8")
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                remote_json = json.loads(remote)
 | 
				
			||||||
 | 
					                remote_version = remote_json["tag_name"]
 | 
				
			||||||
 | 
					                # Basically PEP 616 but that only went in 3.9
 | 
				
			||||||
 | 
					                if remote_version.startswith("ngx-"):
 | 
				
			||||||
 | 
					                    remote_version = remote_version[len("ngx-") :]
 | 
				
			||||||
 | 
					            except ValueError:
 | 
				
			||||||
 | 
					                logger.debug("An error occurred parsing remote version json")
 | 
				
			||||||
 | 
					        except urllib.error.URLError:
 | 
				
			||||||
 | 
					            logger.debug("An error occurred checking for available updates")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        is_greater_than_current = (
 | 
				
			||||||
 | 
					            packaging_version.parse(
 | 
				
			||||||
 | 
					                remote_version,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            > current_version
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response(
 | 
					        return Response(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                "version": remote_version,
 | 
					                "version": remote_version,
 | 
				
			||||||
                "update_available": is_greater_than_current,
 | 
					                "update_available": is_greater_than_current,
 | 
				
			||||||
                "feature_is_set": feature_is_set,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -848,15 +847,23 @@ class UiSettingsView(GenericAPIView):
 | 
				
			|||||||
        displayname = user.username
 | 
					        displayname = user.username
 | 
				
			||||||
        if user.first_name or user.last_name:
 | 
					        if user.first_name or user.last_name:
 | 
				
			||||||
            displayname = " ".join([user.first_name, user.last_name])
 | 
					            displayname = " ".join([user.first_name, user.last_name])
 | 
				
			||||||
        settings = {}
 | 
					        ui_settings = {}
 | 
				
			||||||
        if hasattr(user, "ui_settings"):
 | 
					        if hasattr(user, "ui_settings"):
 | 
				
			||||||
            settings = user.ui_settings.settings
 | 
					            ui_settings = user.ui_settings.settings
 | 
				
			||||||
 | 
					        if "update_checking" in ui_settings:
 | 
				
			||||||
 | 
					            ui_settings["update_checking"][
 | 
				
			||||||
 | 
					                "backend_setting"
 | 
				
			||||||
 | 
					            ] = settings.ENABLE_UPDATE_CHECK
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            ui_settings["update_checking"] = {
 | 
				
			||||||
 | 
					                "backend_setting": settings.ENABLE_UPDATE_CHECK,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        return Response(
 | 
					        return Response(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                "user_id": user.id,
 | 
					                "user_id": user.id,
 | 
				
			||||||
                "username": user.username,
 | 
					                "username": user.username,
 | 
				
			||||||
                "display_name": displayname,
 | 
					                "display_name": displayname,
 | 
				
			||||||
                "settings": settings,
 | 
					                "settings": ui_settings,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -127,10 +127,10 @@ def settings_values_check(app_configs, **kwargs):
 | 
				
			|||||||
                Error(f'OCR output type "{settings.OCR_OUTPUT_TYPE}" is not valid'),
 | 
					                Error(f'OCR output type "{settings.OCR_OUTPUT_TYPE}" is not valid'),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if settings.OCR_MODE not in {"force", "skip", "redo_ocr"}:
 | 
					        if settings.OCR_MODE not in {"force", "skip", "redo", "skip_noarchive"}:
 | 
				
			||||||
            msgs.append(Error(f'OCR output mode "{settings.OCR_MODE}" is not valid'))
 | 
					            msgs.append(Error(f'OCR output mode "{settings.OCR_MODE}" is not valid'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if settings.OCR_CLEAN not in {"clean", "clean_final"}:
 | 
					        if settings.OCR_CLEAN not in {"clean", "clean-final", "none"}:
 | 
				
			||||||
            msgs.append(Error(f'OCR clean mode "{settings.OCR_CLEAN}" is not valid'))
 | 
					            msgs.append(Error(f'OCR clean mode "{settings.OCR_CLEAN}" is not valid'))
 | 
				
			||||||
        return msgs
 | 
					        return msgs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -319,6 +319,7 @@ DATABASES = {
 | 
				
			|||||||
    "default": {
 | 
					    "default": {
 | 
				
			||||||
        "ENGINE": "django.db.backends.sqlite3",
 | 
					        "ENGINE": "django.db.backends.sqlite3",
 | 
				
			||||||
        "NAME": os.path.join(DATA_DIR, "db.sqlite3"),
 | 
					        "NAME": os.path.join(DATA_DIR, "db.sqlite3"),
 | 
				
			||||||
 | 
					        "OPTIONS": {},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -340,21 +341,18 @@ if os.getenv("PAPERLESS_DBHOST"):
 | 
				
			|||||||
    # Leave room for future extensibility
 | 
					    # Leave room for future extensibility
 | 
				
			||||||
    if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
 | 
					    if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
 | 
				
			||||||
        engine = "django.db.backends.mysql"
 | 
					        engine = "django.db.backends.mysql"
 | 
				
			||||||
        options = {"read_default_file": "/etc/mysql/my.cnf"}
 | 
					        options = {"read_default_file": "/etc/mysql/my.cnf", "charset": "utf8mb4"}
 | 
				
			||||||
    else:  # Default to PostgresDB
 | 
					    else:  # Default to PostgresDB
 | 
				
			||||||
        engine = "django.db.backends.postgresql_psycopg2"
 | 
					        engine = "django.db.backends.postgresql_psycopg2"
 | 
				
			||||||
        options = {"sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer")}
 | 
					        options = {"sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer")}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    DATABASES["default"]["ENGINE"] = engine
 | 
					    DATABASES["default"]["ENGINE"] = engine
 | 
				
			||||||
    for key, value in options.items():
 | 
					    DATABASES["default"]["OPTIONS"].update(options)
 | 
				
			||||||
        DATABASES["default"]["OPTIONS"][key] = value
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if os.getenv("PAPERLESS_DB_TIMEOUT") is not None:
 | 
					if os.getenv("PAPERLESS_DB_TIMEOUT") is not None:
 | 
				
			||||||
    _new_opts = {"timeout": float(os.getenv("PAPERLESS_DB_TIMEOUT"))}
 | 
					    DATABASES["default"]["OPTIONS"].update(
 | 
				
			||||||
    if "OPTIONS" in DATABASES["default"]:
 | 
					        {"timeout": float(os.getenv("PAPERLESS_DB_TIMEOUT"))},
 | 
				
			||||||
        DATABASES["default"]["OPTIONS"].update(_new_opts)
 | 
					    )
 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        DATABASES["default"]["OPTIONS"] = _new_opts
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
 | 
					DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
from typing import Final
 | 
					from typing import Final
 | 
				
			||||||
from typing import Tuple
 | 
					from typing import Tuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__: Final[Tuple[int, int, int]] = (1, 9, 0)
 | 
					__version__: Final[Tuple[int, int, int]] = (1, 9, 2)
 | 
				
			||||||
# Version string like X.Y.Z
 | 
					# Version string like X.Y.Z
 | 
				
			||||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
 | 
					__full_version_str__: Final[str] = ".".join(map(str, __version__))
 | 
				
			||||||
# Version string like X.Y
 | 
					# Version string like X.Y
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import tempfile
 | 
				
			|||||||
from datetime import date
 | 
					from datetime import date
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta
 | 
				
			||||||
from fnmatch import fnmatch
 | 
					from fnmatch import fnmatch
 | 
				
			||||||
 | 
					from typing import Dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import magic
 | 
					import magic
 | 
				
			||||||
import pathvalidate
 | 
					import pathvalidate
 | 
				
			||||||
@@ -30,7 +31,7 @@ class MailError(Exception):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BaseMailAction:
 | 
					class BaseMailAction:
 | 
				
			||||||
    def get_criteria(self):
 | 
					    def get_criteria(self) -> Dict:
 | 
				
			||||||
        return {}
 | 
					        return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post_consume(self, M, message_uids, parameter):
 | 
					    def post_consume(self, M, message_uids, parameter):
 | 
				
			||||||
@@ -78,7 +79,7 @@ class TagMailAction(BaseMailAction):
 | 
				
			|||||||
            M.flag(message_uids, [self.keyword], True)
 | 
					            M.flag(message_uids, [self.keyword], True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_rule_action(rule):
 | 
					def get_rule_action(rule) -> BaseMailAction:
 | 
				
			||||||
    if rule.action == MailRule.MailAction.FLAG:
 | 
					    if rule.action == MailRule.MailAction.FLAG:
 | 
				
			||||||
        return FlagMailAction()
 | 
					        return FlagMailAction()
 | 
				
			||||||
    elif rule.action == MailRule.MailAction.DELETE:
 | 
					    elif rule.action == MailRule.MailAction.DELETE:
 | 
				
			||||||
@@ -108,7 +109,7 @@ def make_criterias(rule):
 | 
				
			|||||||
    return {**criterias, **get_rule_action(rule).get_criteria()}
 | 
					    return {**criterias, **get_rule_action(rule).get_criteria()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_mailbox(server, port, security):
 | 
					def get_mailbox(server, port, security) -> MailBox:
 | 
				
			||||||
    if security == MailAccount.ImapSecurity.NONE:
 | 
					    if security == MailAccount.ImapSecurity.NONE:
 | 
				
			||||||
        mailbox = MailBoxUnencrypted(server, port)
 | 
					        mailbox = MailBoxUnencrypted(server, port)
 | 
				
			||||||
    elif security == MailAccount.ImapSecurity.STARTTLS:
 | 
					    elif security == MailAccount.ImapSecurity.STARTTLS:
 | 
				
			||||||
@@ -167,7 +168,7 @@ class MailAccountHandler(LoggingMixin):
 | 
				
			|||||||
                "Unknown correspondent selector",
 | 
					                "Unknown correspondent selector",
 | 
				
			||||||
            )  # pragma: nocover
 | 
					            )  # pragma: nocover
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_mail_account(self, account):
 | 
					    def handle_mail_account(self, account: MailAccount):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.renew_logging_group()
 | 
					        self.renew_logging_group()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -181,7 +182,14 @@ class MailAccountHandler(LoggingMixin):
 | 
				
			|||||||
                account.imap_security,
 | 
					                account.imap_security,
 | 
				
			||||||
            ) as M:
 | 
					            ) as M:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                supports_gmail_labels = "X-GM-EXT-1" in M.client.capabilities
 | 
				
			||||||
 | 
					                supports_auth_plain = "AUTH=PLAIN" in M.client.capabilities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.log("debug", f"GMAIL Label Support: {supports_gmail_labels}")
 | 
				
			||||||
 | 
					                self.log("debug", f"AUTH=PLAIN Support: {supports_auth_plain}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    M.login(account.username, account.password)
 | 
					                    M.login(account.username, account.password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                except UnicodeEncodeError:
 | 
					                except UnicodeEncodeError:
 | 
				
			||||||
@@ -215,7 +223,11 @@ class MailAccountHandler(LoggingMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                for rule in account.rules.order_by("order"):
 | 
					                for rule in account.rules.order_by("order"):
 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
                        total_processed_files += self.handle_mail_rule(M, rule)
 | 
					                        total_processed_files += self.handle_mail_rule(
 | 
				
			||||||
 | 
					                            M,
 | 
				
			||||||
 | 
					                            rule,
 | 
				
			||||||
 | 
					                            supports_gmail_labels,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
                    except Exception as e:
 | 
					                    except Exception as e:
 | 
				
			||||||
                        self.log(
 | 
					                        self.log(
 | 
				
			||||||
                            "error",
 | 
					                            "error",
 | 
				
			||||||
@@ -233,7 +245,12 @@ class MailAccountHandler(LoggingMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return total_processed_files
 | 
					        return total_processed_files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_mail_rule(self, M: MailBox, rule):
 | 
					    def handle_mail_rule(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        M: MailBox,
 | 
				
			||||||
 | 
					        rule: MailRule,
 | 
				
			||||||
 | 
					        supports_gmail_labels: bool = False,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.log("debug", f"Rule {rule}: Selecting folder {rule.folder}")
 | 
					        self.log("debug", f"Rule {rule}: Selecting folder {rule.folder}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -261,11 +278,19 @@ class MailAccountHandler(LoggingMixin):
 | 
				
			|||||||
            ) from err
 | 
					            ) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        criterias = make_criterias(rule)
 | 
					        criterias = make_criterias(rule)
 | 
				
			||||||
        criterias_imap = AND(**criterias)
 | 
					
 | 
				
			||||||
 | 
					        # Deal with the Gmail label extension
 | 
				
			||||||
        if "gmail_label" in criterias:
 | 
					        if "gmail_label" in criterias:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            gmail_label = criterias["gmail_label"]
 | 
					            gmail_label = criterias["gmail_label"]
 | 
				
			||||||
            del criterias["gmail_label"]
 | 
					            del criterias["gmail_label"]
 | 
				
			||||||
            criterias_imap = AND(NOT(gmail_label=gmail_label), **criterias)
 | 
					
 | 
				
			||||||
 | 
					            if not supports_gmail_labels:
 | 
				
			||||||
 | 
					                criterias_imap = AND(**criterias)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                criterias_imap = AND(NOT(gmail_label=gmail_label), **criterias)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            criterias_imap = AND(**criterias)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.log(
 | 
					        self.log(
 | 
				
			||||||
            "debug",
 | 
					            "debug",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										70
									
								
								src/paperless_mail/tests/test_live_mail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/paperless_mail/tests/test_live_mail.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					from paperless_mail.mail import MailAccountHandler
 | 
				
			||||||
 | 
					from paperless_mail.mail import MailError
 | 
				
			||||||
 | 
					from paperless_mail.models import MailAccount
 | 
				
			||||||
 | 
					from paperless_mail.models import MailRule
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Only run if the environment is setup
 | 
				
			||||||
 | 
					# And the environment is not empty (forks, I think)
 | 
				
			||||||
 | 
					@pytest.mark.skipif(
 | 
				
			||||||
 | 
					    "PAPERLESS_MAIL_TEST_HOST" not in os.environ
 | 
				
			||||||
 | 
					    or not len(os.environ["PAPERLESS_MAIL_TEST_HOST"]),
 | 
				
			||||||
 | 
					    reason="Live server testing not enabled",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					class TestMailLiveServer(TestCase):
 | 
				
			||||||
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.mail_account_handler = MailAccountHandler()
 | 
				
			||||||
 | 
					        self.account = MailAccount.objects.create(
 | 
				
			||||||
 | 
					            name="test",
 | 
				
			||||||
 | 
					            imap_server=os.environ["PAPERLESS_MAIL_TEST_HOST"],
 | 
				
			||||||
 | 
					            username=os.environ["PAPERLESS_MAIL_TEST_USER"],
 | 
				
			||||||
 | 
					            password=os.environ["PAPERLESS_MAIL_TEST_PASSWD"],
 | 
				
			||||||
 | 
					            imap_port=993,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return super().setUp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def tearDown(self) -> None:
 | 
				
			||||||
 | 
					        self.account.delete()
 | 
				
			||||||
 | 
					        return super().tearDown()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_process_non_gmail_server_flag(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            rule1 = MailRule.objects.create(
 | 
				
			||||||
 | 
					                name="testrule",
 | 
				
			||||||
 | 
					                account=self.account,
 | 
				
			||||||
 | 
					                action=MailRule.MailAction.FLAG,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.mail_account_handler.handle_mail_account(self.account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            rule1.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        except MailError as e:
 | 
				
			||||||
 | 
					            self.fail(f"Failure: {e}")
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_process_non_gmail_server_tag(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            rule2 = MailRule.objects.create(
 | 
				
			||||||
 | 
					                name="testrule",
 | 
				
			||||||
 | 
					                account=self.account,
 | 
				
			||||||
 | 
					                action=MailRule.MailAction.TAG,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.mail_account_handler.handle_mail_account(self.account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            rule2.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        except MailError as e:
 | 
				
			||||||
 | 
					            self.fail(f"Failure: {e}")
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
@@ -47,15 +47,16 @@ class BogusFolderManager:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BogusClient:
 | 
					class BogusClient:
 | 
				
			||||||
 | 
					    def __init__(self, messages):
 | 
				
			||||||
 | 
					        self.messages: List[MailMessage] = messages
 | 
				
			||||||
 | 
					        self.capabilities: List[str] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __enter__(self):
 | 
					    def __enter__(self):
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
					    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, messages):
 | 
					 | 
				
			||||||
        self.messages: List[MailMessage] = messages
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def authenticate(self, mechanism, authobject):
 | 
					    def authenticate(self, mechanism, authobject):
 | 
				
			||||||
        # authobject must be a callable object
 | 
					        # authobject must be a callable object
 | 
				
			||||||
        auth_bytes = authobject(None)
 | 
					        auth_bytes = authobject(None)
 | 
				
			||||||
@@ -80,12 +81,6 @@ class BogusMailBox(ContextManager):
 | 
				
			|||||||
    # Note the non-ascii characters here
 | 
					    # Note the non-ascii characters here
 | 
				
			||||||
    UTF_PASSWORD: str = "w57äöüw4b6huwb6nhu"
 | 
					    UTF_PASSWORD: str = "w57äöüw4b6huwb6nhu"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __enter__(self):
 | 
					 | 
				
			||||||
        return self
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
        self.messages: List[MailMessage] = []
 | 
					        self.messages: List[MailMessage] = []
 | 
				
			||||||
        self.messages_spam: List[MailMessage] = []
 | 
					        self.messages_spam: List[MailMessage] = []
 | 
				
			||||||
@@ -93,6 +88,12 @@ class BogusMailBox(ContextManager):
 | 
				
			|||||||
        self.client = BogusClient(self.messages)
 | 
					        self.client = BogusClient(self.messages)
 | 
				
			||||||
        self._host = ""
 | 
					        self._host = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __enter__(self):
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def updateClient(self):
 | 
					    def updateClient(self):
 | 
				
			||||||
        self.client = BogusClient(self.messages)
 | 
					        self.client = BogusClient(self.messages)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -648,6 +649,7 @@ class TestMail(DirectoriesMixin, TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def test_handle_mail_account_tag_gmail(self):
 | 
					    def test_handle_mail_account_tag_gmail(self):
 | 
				
			||||||
        self.bogus_mailbox._host = "imap.gmail.com"
 | 
					        self.bogus_mailbox._host = "imap.gmail.com"
 | 
				
			||||||
 | 
					        self.bogus_mailbox.client.capabilities = ["X-GM-EXT-1"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        account = MailAccount.objects.create(
 | 
					        account = MailAccount.objects.create(
 | 
				
			||||||
            name="test",
 | 
					            name="test",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,5 +11,6 @@ def text_consumer_declaration(sender, **kwargs):
 | 
				
			|||||||
        "mime_types": {
 | 
					        "mime_types": {
 | 
				
			||||||
            "text/plain": ".txt",
 | 
					            "text/plain": ".txt",
 | 
				
			||||||
            "text/csv": ".csv",
 | 
					            "text/csv": ".csv",
 | 
				
			||||||
 | 
					            "application/csv": ".csv",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/paperless_tika/tests/samples/sample.docx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/paperless_tika/tests/samples/sample.docx
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/paperless_tika/tests/samples/sample.odt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/paperless_tika/tests/samples/sample.odt
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										78
									
								
								src/paperless_tika/tests/test_live_tika.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/paperless_tika/tests/test_live_tika.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import datetime
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from typing import Final
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					from paperless_tika.parsers import TikaDocumentParser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.skipif("TIKA_LIVE" not in os.environ, reason="No tika server")
 | 
				
			||||||
 | 
					class TestTikaParserAgainstServer(TestCase):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    This test case tests the Tika parsing against a live tika server,
 | 
				
			||||||
 | 
					    if the environment contains the correct value indicating such a server
 | 
				
			||||||
 | 
					    is available.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SAMPLE_DIR: Final[Path] = (Path(__file__).parent / Path("samples")).resolve()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
 | 
					        self.parser = TikaDocumentParser(logging_group=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def tearDown(self) -> None:
 | 
				
			||||||
 | 
					        self.parser.cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_basic_parse_odt(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        GIVEN:
 | 
				
			||||||
 | 
					            - An input ODT format document
 | 
				
			||||||
 | 
					        WHEN:
 | 
				
			||||||
 | 
					            - The document is parsed
 | 
				
			||||||
 | 
					        THEN:
 | 
				
			||||||
 | 
					            - Document content is correct
 | 
				
			||||||
 | 
					            - Document date is correct
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        test_file = self.SAMPLE_DIR / Path("sample.odt")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.parser.parse(test_file, "application/vnd.oasis.opendocument.text")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            self.parser.text,
 | 
				
			||||||
 | 
					            "This is an ODT test document, created September 14, 2022",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertIsNotNone(self.parser.archive_path)
 | 
				
			||||||
 | 
					        with open(self.parser.archive_path, "rb") as f:
 | 
				
			||||||
 | 
					            # PDFs begin with the bytes PDF-x.y
 | 
				
			||||||
 | 
					            self.assertTrue(b"PDF-" in f.read()[:10])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: Unsure what can set the Creation-Date field in a document, enable when possible
 | 
				
			||||||
 | 
					        # self.assertEqual(self.parser.date, datetime.datetime(2022, 9, 14))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_basic_parse_docx(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        GIVEN:
 | 
				
			||||||
 | 
					            - An input DOCX format document
 | 
				
			||||||
 | 
					        WHEN:
 | 
				
			||||||
 | 
					            - The document is parsed
 | 
				
			||||||
 | 
					        THEN:
 | 
				
			||||||
 | 
					            - Document content is correct
 | 
				
			||||||
 | 
					            - Document date is correct
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        test_file = self.SAMPLE_DIR / Path("sample.docx")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.parser.parse(
 | 
				
			||||||
 | 
					            test_file,
 | 
				
			||||||
 | 
					            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            self.parser.text,
 | 
				
			||||||
 | 
					            "This is an DOCX test document, also made September 14, 2022",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertIsNotNone(self.parser.archive_path)
 | 
				
			||||||
 | 
					        with open(self.parser.archive_path, "rb") as f:
 | 
				
			||||||
 | 
					            self.assertTrue(b"PDF-" in f.read()[:10])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # self.assertEqual(self.parser.date, datetime.datetime(2022, 9, 14))
 | 
				
			||||||
		Reference in New Issue
	
	Block a user