Merge branch 'dev' into feature/slim-sidebar

This commit is contained in:
Michael Shamoon 2022-10-09 23:15:48 -07:00
commit a246b3b598
49 changed files with 1699 additions and 874 deletions

@ -1,6 +1,6 @@
{ {
"qpdf": { "qpdf": {
"version": "10.6.3" "version": "11.1.1"
}, },
"jbig2enc": { "jbig2enc": {
"version": "0.29", "version": "0.29",

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

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

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

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

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

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

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

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

Binary file not shown.

Binary file not shown.

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