Merge branch 'dev' into feature-websockets-status

This commit is contained in:
jonaswinkler 2021-01-23 22:22:17 +01:00
commit 05d69c0882
163 changed files with 5843 additions and 2520 deletions
.dockerignore
.github/workflows
.gitignore.readthedocs.yml.travis.ymlDockerfilePipfilePipfile.lockREADME.md
ansible
compile-frontend.shcrowdin.yml
docker
docs
paperless.conf.examplerequirements.txt
scripts
src-ui
angular.jsonmessages.xlfpackage-lock.json
src/app
app.module.ts
components
app-frame
common
dashboard
document-detail
document-list
manage
search
data
pipes

@ -1,3 +1,4 @@
**/__pycache__
/src-ui/.vscode /src-ui/.vscode
/src-ui/node_modules /src-ui/node_modules
/src-ui/dist /src-ui/dist
@ -10,3 +11,9 @@
.pytest_cache .pytest_cache
/dist /dist
/scripts /scripts
/resources
**/tests
**/*.spec.ts
**/htmlcov
/src/.pytest_cache
.idea

64
.github/workflows/ansible.yml vendored Normal file

@ -0,0 +1,64 @@
---
name: Ansible Role
on: [push, pull_request]
jobs:
# https://molecule.readthedocs.io/en/latest/ci.html#github-actions
test:
runs-on: ubuntu-latest
# https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions#github-context
if: github.event_name == 'pull_request' || (github.event_name == 'push' && contains(github.ref, 'refs/heads/'))
steps:
- name: Check out the codebase
uses: actions/checkout@v2
with:
path: "${{ github.repository }}"
- name: Set up Python
uses: actions/setup-python@v2
- name: Set up Docker
uses: docker-practice/actions-setup-docker@master
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install molecule[ansible,docker]
ansible --version
docker --version
molecule --version
python --version
- name: Test fresh installation with molecule
run: |
cd ansible
molecule test -s fresh
working-directory: "${{ github.repository }}"
- name: Test release update with molecule
run: |
cd ansible
molecule test -s update
working-directory: "${{ github.repository }}"
# # https://galaxy.ansible.com/docs/contributing/importing.html
# release:
# runs-on: ubuntu-latest
# needs:
# - test
# # https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions#github-context
# if: contains(github.ref, 'refs/tags/')
# steps:
# - name: Check out the codebase
# uses: actions/checkout@v2
# with:
# path: "${{ github.repository }}"
# - name: Set up Python
# uses: actions/setup-python@v2
# - name: Install dependencies
# run: |
# python3 -m pip install --upgrade ansible-base
# ansible --version
# python --version
# - name: Trigger a new import on Galaxy
# # TODO Check if source if pulled from cwd or imported from github
# # https://github.com/ansible/ansible/blob/devel/lib/ansible/cli/galaxy.py
# run: |
# cd ansible
# ansible-galaxy role import --api-key ${{ secrets.GALAXY_API_KEY }} $(echo ${{ github.repository }} | cut -d/ -f1) $(echo ${{ github.repository }} | cut -d/ -f2)
# working-directory: "${{ github.repository }}"

291
.github/workflows/ci.yml vendored Normal file

@ -0,0 +1,291 @@
name: ci
on: [push, pull_request]
jobs:
documentation:
runs-on: ubuntu-20.04
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
-
name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
-
name: Persistent Github pip cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip3.8}
-
name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends libpoppler-cpp-dev
pip install --upgrade pipenv
pipenv install --system --dev --ignore-pipfile
-
name: Make documentation
run: |
cd docs/
make html
-
name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: documentation
path: docs/_build/html/
tests:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ['3.6', '3.7', '3.8']
fail-fast: false
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "${{ matrix.python-version }}"
-
name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
-
name: Persistent Github pip cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip${{ matrix.python-version }}
-
name: Prepare tests
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends libpoppler-cpp-dev unpaper tesseract-ocr imagemagick ghostscript optipng
pip install --upgrade pipenv
pipenv install --system --dev --ignore-pipfile
-
name: Tests
run: |
cd src/
pytest
-
name: Codestyle
run: |
cd src/
pycodestyle
-
name: Publish coverage results
if: matrix.python-version == '3.8'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/coveralls-clients/coveralls-python/issues/251
run: |
cd src/
coveralls --service=github
frontend:
runs-on: ubuntu-20.04
steps:
-
name: Checkout
uses: actions/checkout@v2
-
uses: actions/setup-node@v2
with:
node-version: '15'
-
name: Build frontend
run: ./compile-frontend.sh
-
name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: frontend-compiled
path: src/documents/static/frontend/
build-release:
needs: [frontend, documentation, tests]
runs-on: ubuntu-20.04
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.7
-
name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends libpoppler-cpp-dev gettext liblept5
pip3 install -r requirements.txt
-
name: Download frontend artifact
uses: actions/download-artifact@v2
with:
name: frontend-compiled
path: src/documents/static/frontend/
-
name: Download documentation artifact
uses: actions/download-artifact@v2
with:
name: documentation
path: docs/_build/html/
-
name: Move files
run: |
mkdir dist
mkdir dist/paperless-ng
mkdir dist/paperless-ng/scripts
cp .dockerignore .env Dockerfile Pipfile Pipfile.lock LICENSE README.md requirements.txt dist/paperless-ng/
cp paperless.conf.example dist/paperless-ng/paperless.conf
cp docker/ dist/paperless-ng/docker -r
cp scripts/*.service scripts/*.sh dist/paperless-ng/scripts/
cp src/ dist/paperless-ng/src -r
cp docs/_build/html/ dist/paperless-ng/docs -r
-
name: Compile messages
run: |
cd dist/paperless-ng/src
python3 manage.py compilemessages
-
name: Collect static files
run: |
cd dist/paperless-ng/src
python3 manage.py collectstatic --no-input
-
name: Make release package
run: |
cd dist
find . -name __pycache__ | xargs rm -r
tar -cJf paperless-ng.tar.xz paperless-ng/
-
name: Upload release artifact
uses: actions/upload-artifact@v2
with:
name: release
path: dist/paperless-ng.tar.xz
publish-release:
runs-on: ubuntu-latest
needs: build-release
if: contains(github.ref, 'refs/tags/ng-')
steps:
-
name: Download release artifact
uses: actions/download-artifact@v2
with:
name: release
path: ./
-
name: Get version
id: get_version
run: |
echo ::set-output name=version::${GITHUB_REF#refs/tags/ng-}
-
name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ng-${{ steps.get_version.outputs.version }}
release_name: Paperless-ng ${{ steps.get_version.outputs.version }}
draft: false
prerelease: false
body: |
For a complete list of changes, see the changelog at https://paperless-ng.readthedocs.io/en/latest/changelog.html.
-
name: Upload release archive
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./paperless-ng.tar.xz
asset_name: paperless-ng-${{ steps.get_version.outputs.version }}.tar.xz
asset_content_type: application/x-xz
# build and push image to docker hub.
build-docker-image:
if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/ng-'))
runs-on: ubuntu-latest
needs: [frontend, tests]
steps:
-
name: Prepare
id: prepare
run: |
IMAGE_NAME=jonaswinkler/paperless-ng
if [[ $GITHUB_REF == refs/tags/ng-* ]]; then
TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/tags/ng-},${IMAGE_NAME}:latest
INSPECT_TAG=${IMAGE_NAME}:latest
elif [[ $GITHUB_REF == refs/heads/* ]]; then
TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/heads/}
INSPECT_TAG=${TAGS}
else
exit 1
fi
echo ::set-output name=tags::${TAGS}
echo ::set-output name=inspect_tag::${INSPECT_TAG}
-
name: Checkout
uses: actions/checkout@v2
-
name: Download frontend artifact
uses: actions/download-artifact@v2
with:
name: frontend-compiled
path: src/documents/static/frontend/
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: ${{ steps.prepare.outputs.tags }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
-
name: Inspect image
run: |
docker buildx imagetools inspect ${{ steps.prepare.outputs.inspect_tag }}

4
.gitignore vendored

@ -65,8 +65,8 @@ target/
.virtualenv .virtualenv
virtualenv virtualenv
/venv /venv
docker-compose.env /docker-compose.env
docker-compose.yml /docker-compose.yml
# Used for development # Used for development
scripts/import-for-development scripts/import-for-development

16
.readthedocs.yml Normal file

@ -0,0 +1,16 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: docs/requirements.txt

@ -1,51 +0,0 @@
language: python
dist: focal
os: linux
jobs:
include:
- name: "Paperless on Python 3.6"
python: "3.6"
- name: "Paperless on Python 3.7"
python: "3.7"
- name: "Paperless on Python 3.8"
python: "3.8"
- name: "Documentation"
script:
- cd docs/
- make html
after_success: true
- name: "Front end"
language: node_js
node_js:
- 15
before_install: true
install:
- cd src-ui/
- npm install -g @angular/cli
- npm install
script:
- ng build --prod
after_success: true
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq libpoppler-cpp-dev unpaper tesseract-ocr imagemagick ghostscript optipng
install:
- pip install --upgrade pipenv
- pipenv install --system --dev
script:
- cd src/
- pipenv run pytest --cov
- pipenv run pycodestyle
after_success:
- pipenv run coveralls

@ -1,48 +1,79 @@
FROM ubuntu:20.04 AS jbig2enc
WORKDIR /usr/src/jbig2enc
RUN apt-get update && apt-get install -y --no-install-recommends build-essential automake libtool libleptonica-dev zlib1g-dev git ca-certificates
RUN git clone https://github.com/agl/jbig2enc .
RUN ./autogen.sh
RUN ./configure && make
FROM python:3.7-slim FROM python:3.7-slim
WORKDIR /usr/src/paperless/ WORKDIR /usr/src/paperless/
COPY requirements.txt ./ COPY requirements.txt ./
#Dependencies # Binary dependencies
RUN apt-get update \ RUN apt-get update \
&& apt-get -y --no-install-recommends install \ && apt-get -y --no-install-recommends install \
build-essential \ # Basic dependencies
curl \ curl \
file \ file \
# fonts for text file thumbnail generation
fonts-liberation \ fonts-liberation \
ghostscript \ # for making translations further down
gettext \
gnupg \ gnupg \
icc-profiles-free \
imagemagick \ imagemagick \
# for Numpy
libatlas-base-dev \ libatlas-base-dev \
liblept5 \
libmagic-dev \
libpoppler-cpp-dev \
libpq-dev \
libqpdf-dev \
libxml2 \
libxslt1-dev \ libxslt1-dev \
mime-support \
# thumbnail size reduction
optipng \ optipng \
sudo \
tzdata \
# OCRmyPDF dependencies
ghostscript \
icc-profiles-free \
liblept5 \
libxml2 \
pngquant \ pngquant \
qpdf \ qpdf \
sudo \
tesseract-ocr \ tesseract-ocr \
tesseract-ocr-eng \ tesseract-ocr-eng \
tesseract-ocr-deu \ tesseract-ocr-deu \
tesseract-ocr-fra \ tesseract-ocr-fra \
tesseract-ocr-ita \ tesseract-ocr-ita \
tesseract-ocr-spa \ tesseract-ocr-spa \
tzdata \
unpaper \ unpaper \
zlib1g \ zlib1g \
&& pip3 install --upgrade supervisor setuptools \ && rm -rf /var/lib/apt/lists/*
&& pip install --no-cache-dir -r requirements.txt \
# This pulls in updated dependencies from bullseye to fix some issues with file type detection.
# TODO: Remove this once bullseye releases.
RUN echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.list.d/bullseye.list \
&& apt-get update \
&& apt-get install --no-install-recommends -y file libmagic-dev \
&& rm -rf /var/lib/apt/lists/* \
&& rm /etc/apt/sources.list.d/bullseye.list
# Python dependencies
RUN apt-get update \
&& apt-get -y --no-install-recommends install \
build-essential \
libpoppler-cpp-dev \
libpq-dev \
libqpdf-dev \
&& python3 -m pip install --upgrade --no-cache-dir supervisor \
&& python3 -m pip install --no-cache-dir -r requirements.txt \
&& apt-get -y purge build-essential libqpdf-dev \ && apt-get -y purge build-essential libqpdf-dev \
&& apt-get -y autoremove --purge \ && apt-get -y autoremove --purge \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& mkdir /var/log/supervisord /var/run/supervisord && mkdir /var/log/supervisord /var/run/supervisord
# copy scripts # copy scripts
# this fixes issues with imagemagick and PDF # this fixes issues with imagemagick and PDF
COPY docker/imagemagick-policy.xml /etc/ImageMagick-6/policy.xml COPY docker/imagemagick-policy.xml /etc/ImageMagick-6/policy.xml
@ -50,6 +81,12 @@ COPY docker/gunicorn.conf.py ./
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/docker-entrypoint.sh /sbin/docker-entrypoint.sh COPY docker/docker-entrypoint.sh /sbin/docker-entrypoint.sh
# copy jbig2enc
COPY --from=jbig2enc /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/
COPY --from=jbig2enc /usr/src/jbig2enc/src/jbig2 /usr/local/bin/
COPY --from=jbig2enc /usr/src/jbig2enc/src/*.h /usr/local/include/
# copy app # copy app
COPY src/ ./src/ COPY src/ ./src/

14
Pipfile

@ -8,9 +8,6 @@ url = "https://www.piwheels.org/simple"
verify_ssl = true verify_ssl = true
name = "piwheels" name = "piwheels"
[requires]
python_version = "3.6"
[packages] [packages]
dateparser = "~=0.7.6" dateparser = "~=0.7.6"
django = "~=3.1.3" django = "~=3.1.3"
@ -26,8 +23,9 @@ imap-tools = "*"
langdetect = "*" langdetect = "*"
pdftotext = "*" pdftotext = "*"
pathvalidate = "*" pathvalidate = "*"
pillow = "*" # pinned to 8.1.0, since aarch64 wheels might not be available beyond that https://github.com/python-pillow/Pillow/issues/5202
pikepdf = "*" pillow = "==8.1.0"
pikepdf = "~=2.2.5"
python-gnupg = "*" python-gnupg = "*"
python-dotenv = "*" python-dotenv = "*"
python-dateutil = "*" python-dateutil = "*"
@ -35,12 +33,12 @@ python-Levenshtein = "*"
python-magic = "*" python-magic = "*"
psycopg2-binary = "*" psycopg2-binary = "*"
redis = "*" redis = "*"
scikit-learn="~=0.23.2" scikit-learn="~=0.24.0"
whitenoise = "~=5.2.0" whitenoise = "~=5.2.0"
watchdog = "*" watchdog = "*"
whoosh="~=2.7.4" whoosh="~=2.7.4"
inotifyrecursive = "~=0.3.4" inotifyrecursive = "~=0.3.4"
ocrmypdf = "*" ocrmypdf = "~=11.4.5"
tqdm = "*" tqdm = "*"
tika = "*" tika = "*"
channels = "~=3.0" channels = "~=3.0"
@ -57,6 +55,6 @@ pytest-django = "*"
pytest-env = "*" pytest-env = "*"
pytest-sugar = "*" pytest-sugar = "*"
pytest-xdist = "*" pytest-xdist = "*"
sphinx = "~=3.3" sphinx = "~=3.4.2"
sphinx_rtd_theme = "*" sphinx_rtd_theme = "*"
tox = "*" tox = "*"

655
Pipfile.lock generated

@ -1,12 +1,10 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "8dbe59054648cdfec443bad9f73c25263e43f18f35e74dc72a8429a0035ecbeb" "sha256": "3c85a487240f18b3feb44f8899395696cb79630f320f0df9ef5ee37b914c89f2"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {},
"python_version": "3.6"
},
"sources": [ "sources": [
{ {
"name": "pypi", "name": "pypi",
@ -21,13 +19,6 @@
] ]
}, },
"default": { "default": {
"aioredis": {
"hashes": [
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
"sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
],
"version": "==1.3.1"
},
"arrow": { "arrow": {
"hashes": [ "hashes": [
"sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5", "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5",
@ -44,38 +35,6 @@
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==3.3.1" "version": "==3.3.1"
}, },
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"autobahn": {
"hashes": [
"sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895",
"sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049"
],
"markers": "python_version >= '3.6'",
"version": "==20.12.3"
},
"automat": {
"hashes": [
"sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33",
"sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111",
"sha256:d6d976cf8da698fc85fa7def46e2544493f78cb7ee72d2f4acd1a5c759a3060e"
],
"version": "==20.2.0"
},
"blessed": { "blessed": {
"hashes": [ "hashes": [
"sha256:0a74a8d3f0366db600d061273df77d44f0db07daade7bb7a4d49c8bc22ed9f74", "sha256:0a74a8d3f0366db600d061273df77d44f0db07daade7bb7a4d49c8bc22ed9f74",
@ -133,22 +92,6 @@
], ],
"version": "==1.14.4" "version": "==1.14.4"
}, },
"channels": {
"hashes": [
"sha256:056b72e51080a517a0f33a0a30003e03833b551d75394d6636c885d4edb8188f",
"sha256:3f15bdd2138bb4796e76ea588a0a344b12a7964ea9b2e456f992fddb988a4317"
],
"index": "pypi",
"version": "==3.0.3"
},
"channels-redis": {
"hashes": [
"sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de",
"sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6"
],
"index": "pypi",
"version": "==3.2.0"
},
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
@ -165,13 +108,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==15.0" "version": "==15.0"
}, },
"constantly": {
"hashes": [
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
"sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"
],
"version": "==15.1.0"
},
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d", "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d",
@ -193,14 +129,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==3.3.1" "version": "==3.3.1"
}, },
"daphne": {
"hashes": [
"sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a",
"sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"
],
"index": "pypi",
"version": "==3.0.1"
},
"dateparser": { "dateparser": {
"hashes": [ "hashes": [
"sha256:7552c994f893b5cb8fcf103b4cd2ff7f57aab9bfd2619fdf0cf571c0740fd90b", "sha256:7552c994f893b5cb8fcf103b4cd2ff7f57aab9bfd2619fdf0cf571c0740fd90b",
@ -259,7 +187,8 @@
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7" "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7",
"sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.12.2" "version": "==3.12.2"
@ -288,60 +217,6 @@
"index": "pypi", "index": "pypi",
"version": "==20.0.4" "version": "==20.0.4"
}, },
"hiredis": {
"hashes": [
"sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680",
"sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0",
"sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0",
"sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01",
"sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a",
"sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b",
"sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6",
"sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73",
"sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee",
"sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55",
"sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12",
"sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b",
"sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323",
"sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c",
"sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655",
"sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5",
"sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75",
"sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb",
"sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23",
"sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1",
"sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f",
"sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872",
"sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058",
"sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454",
"sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882",
"sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2",
"sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132",
"sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6",
"sha256:9f4e67f87e072de981570eaf7cb41444bbac7e92b05c8651dbab6eb1fb8d5a14",
"sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c",
"sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363",
"sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3",
"sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4",
"sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919",
"sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349",
"sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae",
"sha256:b39989b49e8aca9d224324d2650029eda410a4faf43f6afb0eb4f9acb7be6097",
"sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da",
"sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f",
"sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed",
"sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628",
"sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64",
"sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86",
"sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf",
"sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c",
"sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded",
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"humanfriendly": { "humanfriendly": {
"hashes": [ "hashes": [
"sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d", "sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d",
@ -350,14 +225,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==9.1" "version": "==9.1"
}, },
"hyperlink": {
"hashes": [
"sha256:402c1b5fa066ea368f3118fc5a6f8505440b4d1a4ef12a844ca39332a4a29944",
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
"sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63"
],
"version": "==20.0.1"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226", "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226",
@ -382,12 +249,13 @@
], ],
"version": "==0.4.0" "version": "==0.4.0"
}, },
"incremental": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771",
"sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"
], ],
"version": "==17.5.0" "markers": "python_version < '3.8'",
"version": "==3.4.0"
}, },
"inotify-simple": { "inotify-simple": {
"hashes": [ "hashes": [
@ -467,89 +335,54 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.6.2" "version": "==4.6.2"
}, },
"msgpack": {
"hashes": [
"sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9",
"sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841",
"sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439",
"sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694",
"sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a",
"sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f",
"sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e",
"sha256:7307e86f7ce75b49e65b55660b10b258e9e7b5e0f80d31d7a86a278d8204d1b4",
"sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1",
"sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c",
"sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b",
"sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759",
"sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326",
"sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc",
"sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192",
"sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83",
"sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06",
"sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e",
"sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9",
"sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33",
"sha256:c82dc0ba34d620fb94d12a7725e9362958bb1be3938688a061f53ed86bee005a",
"sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54",
"sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f",
"sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887",
"sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009",
"sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2",
"sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c",
"sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87",
"sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984",
"sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"
],
"version": "==1.0.2"
},
"numpy": { "numpy": {
"hashes": [ "hashes": [
"sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db", "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94",
"sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce", "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080",
"sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1", "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e",
"sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512", "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c",
"sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2", "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76",
"sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757", "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371",
"sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9", "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c",
"sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2", "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2",
"sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08", "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a",
"sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b", "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb",
"sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb", "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140",
"sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc", "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28",
"sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac", "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f",
"sha256:5ddd1dfa2be066595c1993165b4cae84b9866b12339d0c903db7f21a094324a3", "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d",
"sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83", "sha256:6373751c4b6fd325606d29dd98dc2bf7092485ad20aafbfc6a177acd3b89059e",
"sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36", "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff",
"sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387", "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8",
"sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f", "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa",
"sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad", "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea",
"sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c", "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc",
"sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414", "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73",
"sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37", "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d",
"sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764", "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d",
"sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753", "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4",
"sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909", "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c",
"sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6", "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e",
"sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63", "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea",
"sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9", "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd",
"sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949", "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f",
"sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab", "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff",
"sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c", "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e",
"sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3", "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7",
"sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893", "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa",
"sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15", "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827",
"sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4" "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==1.19.4" "version": "==1.19.5"
}, },
"ocrmypdf": { "ocrmypdf": {
"hashes": [ "hashes": [
"sha256:161c9dffb61485d30d4caea07dcb6d1b73ffa43f6e8767504a9128c510cc0c8c", "sha256:416a9c4321bfc844f250694b8c68ebb538f60609bbc8686bd9f84a13c5127d68",
"sha256:404e564d0eac076cc520f0742b3e711f2611ae12a7adbc05f1232a77a81d6d61" "sha256:f45fc7e844e6026d6080a623a2936be120fc077d99aaa599df022acf35fb31e6"
], ],
"index": "pypi", "index": "pypi",
"version": "==11.4.4" "version": "==11.4.5"
}, },
"pathvalidate": { "pathvalidate": {
"hashes": [ "hashes": [
@ -576,34 +409,35 @@
}, },
"pikepdf": { "pikepdf": {
"hashes": [ "hashes": [
"sha256:05fac9db7d5f5871f7b6598714386ffe56c1589e1d984859fb9e6a4ec8f0ebd0", "sha256:0e67e5beeeed5422b3b8e862e4777fed5a4cd3c72e711e2a449a65d9ee641448",
"sha256:267f76dc2ca107498d9cd90df8b26d36c57faebff933ef4069dffa8d2e14a9e4", "sha256:138155ae1f71634cd6eca79f5517f77b2067ef0bd5b627ea9414e308fe868dc5",
"sha256:28d9f436086faf03306d321465a9384aaefe7fb023a46fc177921bc899656c6b", "sha256:15cf648dd760a47c55a4106b601b92bb653ae98155b10f04310553629c6695dd",
"sha256:2e66e15122f18b1dfbe6f48b90ebfd72c666b16330af5c4849e9b9aa930c8983", "sha256:1d6a011ae4c501c78509caf19cbe152c2e3cb5c267f7b47bc3db8cd3436585a7",
"sha256:3147bd0b4f4c6ed42b8dce724aa76d041aa071ebf4b500da302e1b368eb57811", "sha256:211f529313953e44ae42eb896c2b688668385e6e8f9d04d21484bddb3c42b34c",
"sha256:385da233cb211f00a154597b437214392b25ba83b88da53124ff01856f4e0753", "sha256:22049ad288d603a7fc68e90a0722770d307886788373ddfe71fbf614ced0f5b2",
"sha256:497000a07a1549239a83b3753e38b30257a5978d0c3f1b0ddaf698c2e1722616", "sha256:24f7c371f6ecbee8f0ae30030992fc75cd32cd575dcfca8d466a03a8290377ca",
"sha256:497c2d9212ec4d08582bdb4bb75d383de9f3d91308092dd23b84fdecffc08fbc", "sha256:26cdf561632866d584fedb6b1c1fce78cefa49b5cae54c65aa6a6ca5fe6de4ac",
"sha256:62df5bed7aefbfadf29063d1c6bb9d5132bea0f6f40a186b75e068805ba96d45", "sha256:2c37afcd21a2eb1da1773687e853327fa8ec7d2c5cd90cdcd70180f55f0221e1",
"sha256:80380933b1423adb25ebee33659614b9e4cd7fdfb655184d5bb8becc2ea5109a", "sha256:65b8ec6403814f51e1b9c7e18a8ff26087fcc7a199b1405583e5ff9eb931db56",
"sha256:8a72fff7adff10f7459670cc7950988cb2863ccfef107460432a7f290d00a9a1", "sha256:66a03103aadb2e2738271cb18c89837ac3980fa0b4687195c4c150228b7e79de",
"sha256:a59fe04e67db87a63bc9f3722210e672c0b0577707e51dd121d1480afdec0c28", "sha256:6e8f0124354c53a66f83ec5a18111b760aeff1a64db3a86e7ee5fed8e8624707",
"sha256:ac163f12a1e07a441976261367e2dfd374e050ec81a199099b9ef01143d3b01b", "sha256:70f2836cd468aa25bc8b09a2b9561364bd75d3e6ddb0e50a25d248d7da6cff25",
"sha256:b63b0f6a73df3533181c310af48a5acc6acdb64deb3a36e4082264a7e98f3ca2", "sha256:82cebf68952cfb65c86d880eb782a0c558b37531cdae59f2e11fcd0f2bb4669c",
"sha256:c3bba19636181cbe9b20dd382eec2c64c1df7ae410089c63ee20aa1d5d14dfa4", "sha256:84ad3e8fd5f3251fb5b534614da64b04a264ce9348f0fe35b781c0fb378b0f82",
"sha256:c8f70fb7453825bcbbe77da56132a22567d4ffbfe8ab8cb801d06fb56b624f6a", "sha256:af13fbc022efa85d1ae161129d4cde66493479db52b9adb74d525b890a078208",
"sha256:dd6dd1c15f770da01c03531095b8fbd1932df225297dc13f4987ca1260c2d723", "sha256:c1d40fb8f8192c75f54f0e74a569ccf45e4e13bed8da78a78a5b488be29979bf",
"sha256:e6f5dc7e2a969e73134f7fd7876a7bd2a186e6284e0ed56745d7836626abed15", "sha256:d147ec1ab58512871fdf40a161809f698eaa75720b4a230198e7e028582b20a1",
"sha256:ef8f2935b4380b3ed797bfbb12d143cf01fe62bdec14018813fd4cb029495999", "sha256:dedad1f68d6b0b54000f7f99386351f1c6e19c8cf70a9700d8dd06b9809c54fb",
"sha256:f2a75b290f2740ccaad077240ec8d5f963991efd63369b2e4b5d2d046b22632e", "sha256:e72c3f5b624b9c7341fd6a7e657926d4cf12a7ea453681ffd7332cabc3530c62",
"sha256:f81ea51e868f075515bc9f805710105ca759fc01c29ee3cd500186a2d17e21c2" "sha256:eb75f22e261b3bc69b6fc9a17b1d6966c95e79d3e792b7737a018a2bf6a2b07f"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.4" "version": "==2.2.5"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6", "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
"sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded",
"sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865", "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865",
"sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174", "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174",
"sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032", "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032",
@ -629,9 +463,12 @@
"sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8", "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8",
"sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59", "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59",
"sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d", "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d",
"sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7",
"sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a", "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a",
"sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0",
"sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b", "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b",
"sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d" "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d",
"sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"
], ],
"index": "pypi", "index": "pypi",
"version": "==8.1.0" "version": "==8.1.0"
@ -687,42 +524,6 @@
"index": "pypi", "index": "pypi",
"version": "==2.8.6" "version": "==2.8.6"
}, },
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
],
"version": "==0.2.8"
},
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
@ -731,22 +532,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20" "version": "==2.20"
}, },
"pyhamcrest": {
"hashes": [
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
],
"markers": "python_version >= '3.5'",
"version": "==2.0.2"
},
"pyopenssl": {
"hashes": [
"sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",
"sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.0.1"
},
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
@ -910,59 +695,67 @@
}, },
"scikit-learn": { "scikit-learn": {
"hashes": [ "hashes": [
"sha256:090bbf144fd5823c1f2efa3e1a9bf180295b24294ca8f478e75b40ed54f8036e", "sha256:076369634ee72b5a5941440661e2f306ff4ac30903802dc52031c7e9199ac640",
"sha256:0a127cc70990d4c15b1019680bfedc7fec6c23d14d3719fdf9b64b22d37cdeca", "sha256:18f7131e62265bf2691ed1d0303c640313894ccfe4278427478c6b2f45094b53",
"sha256:0d39748e7c9669ba648acf40fb3ce96b8a07b240db6888563a7cb76e05e0d9cc", "sha256:26f66b3726b54dfb76ea51c5d9c2431ed17ebc066cb4527662b9e851a3e7ba61",
"sha256:1b8a391de95f6285a2f9adffb7db0892718950954b7149a70c783dc848f104ea", "sha256:2951f87d35e72f007701c6e028aa230f6df6212a3194677c0c950486066a454d",
"sha256:20766f515e6cd6f954554387dfae705d93c7b544ec0e6c6a5d8e006f6f7ef480", "sha256:2a5348585aa793bc8cc5a72f8e9067c9380834b0aadbd55f924843b071f13282",
"sha256:2aa95c2f17d2f80534156215c87bee72b6aa314a7f8b8fe92a2d71f47280570d", "sha256:3eeff086f7329521d27249a082ea3c48c085cedb110db5f65968ab55c3ba2e09",
"sha256:5ce7a8021c9defc2b75620571b350acc4a7d9763c25b7593621ef50f3bd019a2", "sha256:4395e91b3548005f4a645018435b5a94f8cce232b5b70753020e606c6a750656",
"sha256:6c28a1d00aae7c3c9568f61aafeaad813f0f01c729bee4fd9479e2132b215c1d", "sha256:44e452ea8491225c5783d49577aad0f36202dfd52aec7f82c0fdfe5fbd5f7400",
"sha256:7671bbeddd7f4f9a6968f3b5442dac5f22bf1ba06709ef888cc9132ad354a9ab", "sha256:490436b44b3a1957cb625e871764b0aa330b34cc416aea4abc6c38ca63d0d682",
"sha256:914ac2b45a058d3f1338d7736200f7f3b094857758895f8667be8a81ff443b5b", "sha256:5e6e3c042cea83f2e20a45e563b8eabc1f8f72446251fe23ebefdf111a173a33",
"sha256:98508723f44c61896a4e15894b2016762a55555fbf09365a0bb1870ecbd442de", "sha256:66f27bf21202a850bcd7b6303916e4907f6e22ec59a14974ede4955aed5c7ed0",
"sha256:a64817b050efd50f9abcfd311870073e500ae11b299683a519fbb52d85e08d25", "sha256:743b6edd98c98991be46c08e6b21df3861d5ae915f91d59f988384d93f7263e7",
"sha256:cb3e76380312e1f86abd20340ab1d5b3cc46a26f6593d3c33c9ea3e4c7134028", "sha256:758619e49cd7c17282e6cc60d5cc73c02c072b47c9a10010bb3bb47e0d976e50",
"sha256:d0dcaa54263307075cb93d0bee3ceb02821093b1b3d25f66021987d305d01dce", "sha256:7f654befc5ad413690cc58f3f34a3e906caf825195ce0fda00a8e9565e1403e6",
"sha256:d9a1ce5f099f29c7c33181cc4386660e0ba891b21a60dc036bf369e3a3ee3aec", "sha256:800aaf63f8838c00e85db2267dd226f89858594843fd03932a9eda95746d2c40",
"sha256:da8e7c302003dd765d92a5616678e591f347460ac7b53e53d667be7dfe6d1b10", "sha256:80ca024154b84b6ac4cfc86930ba13fdc348a209753bf2c16129db6f9eb8a80b",
"sha256:daf276c465c38ef736a79bd79fc80a249f746bcbcae50c40945428f7ece074f8" "sha256:890d7d588f65acb0c4f6c083347c9076916bda5e6bd8400f06244b1afc1009af",
"sha256:905d8934d1e27a686698864a5863ff2c0e13a2ae1adb78a8a848aacc8a49927d",
"sha256:a83fcd9d59c42a2f66b307e3b0b0f08aa8e6e45be33da055697ea499f0e4f7c2",
"sha256:afeb06dc69847927634e58579b9cdc72e1390b79497336b2324b1b173f33bd47",
"sha256:b0d13fd56d26cf3de0314a4fd48037108c638fe126d813f5c1222bb0f08b6a76",
"sha256:c08b27cb78ee8d2dc781a7affed09859441f5b624f9f92da59ac0791c8774dfc",
"sha256:c912247e42114f389858ae05d63f4359d4e667ea72aaabee191aee9ad3f9774a",
"sha256:d7fe05fcb44eadd6d6c874c768f085f5de1239db3a3b7be4d3d23d12e4120589",
"sha256:d819d625832fb2969911a243e009cfa135cb8ef1e150866e417d6e9d75290087",
"sha256:e534f5f3796db6781c87e9835dcd51b7854c8c5a379c9210b93605965c1941fd"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.23.2" "version": "==0.24.0"
}, },
"scipy": { "scipy": {
"hashes": [ "hashes": [
"sha256:155225621df90fcd151e25d51c50217e412de717475999ebb76e17e310176981", "sha256:168c45c0c32e23f613db7c9e4e780bc61982d71dcd406ead746c7c7c2f2004ce",
"sha256:1bc5b446600c4ff7ab36bade47180673141322f0febaa555f1c433fe04f2a0e3", "sha256:213bc59191da2f479984ad4ec39406bf949a99aba70e9237b916ce7547b6ef42",
"sha256:2f1c2ebca6fd867160e70102200b1bd07b3b2d31a3e6af3c58d688c15d0d07b7", "sha256:25b241034215247481f53355e05f9e25462682b13bd9191359075682adcd9554",
"sha256:313785c4dab65060f9648112d025f6d2fec69a8a889c714328882d678a95f053", "sha256:2c872de0c69ed20fb1a9b9cf6f77298b04a26f0b8720a5457be08be254366c6e",
"sha256:31ab217b5c27ab429d07428a76002b33662f98986095bbce5d55e0788f7e8b15", "sha256:3397c129b479846d7eaa18f999369a24322d008fac0782e7828fa567358c36ce",
"sha256:3d4303e3e21d07d9557b26a1707bb9fc065510ee8501c9bf22a0157249a82fd0", "sha256:368c0f69f93186309e1b4beb8e26d51dd6f5010b79264c0f1e9ca00cd92ea8c9",
"sha256:4f1d9cc977ac6a4a63c124045c1e8bf67ec37098f67c699887a93736961a00ae", "sha256:3d5db5d815370c28d938cf9b0809dade4acf7aba57eaf7ef733bfedc9b2474c4",
"sha256:58731bbe0103e96b89b2f41516699db9b63066e4317e31b8402891571f6d358f", "sha256:4598cf03136067000855d6b44d7a1f4f46994164bcd450fb2c3d481afc25dd06",
"sha256:8629135ee00cc2182ac8be8e75643b9f02235942443732c2ed69ab48edcb6614", "sha256:4a453d5e5689de62e5d38edf40af3f17560bfd63c9c5bd228c18c1f99afa155b",
"sha256:876badc33eec20709d4e042a09834f5953ebdac4088d45a4f3a1f18b56885718", "sha256:4f12d13ffbc16e988fa40809cbbd7a8b45bc05ff6ea0ba8e3e41f6f4db3a9e47",
"sha256:8840a9adb4ede3751f49761653d3ebf664f25195fdd42ada394ffea8903dd51d", "sha256:634568a3018bc16a83cda28d4f7aed0d803dd5618facb36e977e53b2df868443",
"sha256:aef3a2dbc436bbe8f6e0b635f0b5fe5ed024b522eee4637dbbe0b974129ca734", "sha256:65923bc3809524e46fb7eb4d6346552cbb6a1ffc41be748535aa502a2e3d3389",
"sha256:b8af26839ae343655f3ca377a5d5e5466f1d3b3ac7432a43449154fe958ae0e0", "sha256:6b0ceb23560f46dd236a8ad4378fc40bad1783e997604ba845e131d6c680963e",
"sha256:c0911f3180de343643f369dc5cfedad6ba9f939c2d516bddea4a6871eb000722", "sha256:8c8d6ca19c8497344b810b0b0344f8375af5f6bb9c98bd42e33f747417ab3f57",
"sha256:cb6dc9f82dfd95f6b9032a8d7ea70efeeb15d5b5fd6ed4e8537bb3c673580566", "sha256:9ad4fcddcbf5dc67619379782e6aeef41218a79e17979aaed01ed099876c0e62",
"sha256:cdbc47628184a0ebeb5c08f1892614e1bd4a51f6e0d609c6eed253823a960f5b", "sha256:a254b98dbcc744c723a838c03b74a8a34c0558c9ac5c86d5561703362231107d",
"sha256:d902d3a5ad7f28874c0a82db95246d24ca07ad932741df668595fe00a4819870", "sha256:b03c4338d6d3d299e8ca494194c0ae4f611548da59e3c038813f1a43976cb437",
"sha256:eab389aba0ad8b5e6b5abdc3337ade46823df75f80a8edd4c67833567577cb3d", "sha256:b5e9d3e4474644915809d6aa1416ff20430a3ed9ae723a5d295da5ddb24985e2",
"sha256:eb7928275f3560d47e5538e15e9f32b3d64cd30ea8f85f3e82987425476f53f6", "sha256:cc1f78ebc982cd0602c9a7615d878396bec94908db67d4ecddca864d049112f2",
"sha256:f68d5761a2d2376e2b194c8e9192bbf7c51306ca176f1a0889990a52ef0d551f" "sha256:d6d25c41a009e3c6b7e757338948d0076ee1dd1770d1c09ec131f11946883c54",
"sha256:d84cadd7d7998433334c99fa55bcba0d8b4aeff0edb123b2a1dfcface538e474",
"sha256:e360cb2299028d0b0d0f65a5c5e51fc16a335f1603aa2357c25766c8dab56938",
"sha256:e98d49a5717369d8241d6cf33ecb0ca72deee392414118198a8e5b4c35c56340",
"sha256:ed572470af2438b526ea574ff8f05e7f39b44ac37f712105e57fc4d53a6fb660",
"sha256:f87b39f4d69cf7d7529d7b1098cb712033b17ea7714aed831b95628f483fd012",
"sha256:fa789583fc94a7689b45834453fec095245c7e69c58561dc159b5d5277057e4c"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.6'",
"version": "==1.6.0" "version": "==1.5.4"
},
"service-identity": {
"hashes": [
"sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36",
"sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d"
],
"version": "==18.1.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -1005,53 +798,20 @@
}, },
"tqdm": { "tqdm": {
"hashes": [ "hashes": [
"sha256:556c55b081bd9aa746d34125d024b73f0e2a0e62d5927ff0e400e20ee0a03b9a", "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a",
"sha256:b8b46036fd00176d0870307123ef06bb851096964fa7fc578d789f90ce82c3e4" "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.55.1" "version": "==4.56.0"
}, },
"twisted": { "typing-extensions": {
"extras": [
"tls"
],
"hashes": [ "hashes": [
"sha256:0150dae5adc962d15e00054cc6926f1e64763fb8dd26e1632593ac06e592104b", "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
"sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c",
"sha256:15e52271f08f62e2230ff093e0278aa01c9dac057c4557cadadd2429eed86a3e",
"sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292",
"sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22",
"sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec",
"sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478",
"sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2",
"sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29",
"sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114",
"sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797",
"sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa",
"sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15",
"sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd",
"sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274",
"sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad",
"sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7",
"sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a",
"sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10",
"sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780",
"sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504",
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
"sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version < '3.8'",
"version": "==20.3.0" "version": "==3.7.4.3"
},
"txaio": {
"hashes": [
"sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
"sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
],
"markers": "python_version >= '3.6'",
"version": "==20.12.1"
}, },
"tzlocal": { "tzlocal": {
"hashes": [ "hashes": [
@ -1115,65 +875,13 @@
"index": "pypi", "index": "pypi",
"version": "==2.7.4" "version": "==2.7.4"
}, },
"zope.interface": { "zipp": {
"hashes": [ "hashes": [
"sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1", "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
"sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d", "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
"sha256:09fc3922f235703c0b76f8234867685eee68a24a49fffa2220975f6142db45f1",
"sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
"sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
"sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
"sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
"sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
"sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
"sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
"sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
"sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
"sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
"sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
"sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
"sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
"sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
"sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
"sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
"sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
"sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
"sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
"sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
"sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
"sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
"sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
"sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
"sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
"sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
"sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
"sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
"sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
"sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
"sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
"sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
"sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
"sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
"sha256:974f5957e66a7524ea81df7b2686a456bfaf0408dbb7353ddfbedb594eadfef6",
"sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
"sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
"sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
"sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
"sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
"sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
"sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
"sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
"sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
"sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
"sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
"sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
"sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
"sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '3.6'",
"version": "==5.2.0" "version": "==3.4.0"
} }
}, },
"develop": { "develop": {
@ -1289,11 +997,11 @@
}, },
"coveralls": { "coveralls": {
"hashes": [ "hashes": [
"sha256:2301a19500b06649d2ec4f2858f9c69638d7699a4c63027c5d53daba666147cc", "sha256:5399c0565ab822a70a477f7031f6c88a9dd196b3de2877b3facb43b51bd13434",
"sha256:b990ba1f7bc4288e63340be0433698c1efe8217f78c689d254c2540af3d38617" "sha256:f8384968c57dee4b7133ae701ecdad88e85e30597d496dcba0d7fbb470dca41f"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.0" "version": "==3.0.0"
}, },
"distlib": { "distlib": {
"hashes": [ "hashes": [
@ -1335,11 +1043,11 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:7b0c4bb678be21a68640007f254259c73d18f7996a3448267716423360519732", "sha256:47ac7d62d5bad8c16422a91f121430ab7656d40ca8fea9c84bcdbdf92e739b03",
"sha256:7e98483fc273ec5cfe1c9efa9b99adaa2de4c6b610fbc62d3767088e4974b0ce" "sha256:6bc44606d44f711e1d89ad9a5b42394cc6f7eedaffc765ddb5b2d22084c15733"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==5.3.0" "version": "==5.5.0"
}, },
"filelock": { "filelock": {
"hashes": [ "hashes": [
@ -1366,6 +1074,22 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"importlib-metadata": {
"hashes": [
"sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771",
"sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"
],
"markers": "python_version < '3.8'",
"version": "==3.4.0"
},
"importlib-resources": {
"hashes": [
"sha256:4743f090ed8946e713745ec0e660249ef9fb0b9843eacc5b5ff931d2fd5aa67f",
"sha256:ea17df80a0ff04b5dbd3d96dbeab1842acfd1c6c902eaeb8c8858abf2720161e"
],
"markers": "python_version < '3.7'",
"version": "==5.0.0"
},
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
@ -1459,11 +1183,11 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435",
"sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08" "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==2.7.3" "version": "==2.7.4"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
@ -1570,19 +1294,19 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:77dec5ac77ca46eee54f59cf477780f4fb23327b3339ef39c8471abb829c1285", "sha256:41cad293f954f7d37f803d97eb184158cfd90f51195131e94875bc07cd08b93c",
"sha256:b8aa4eb5502c53d3b5ca13a07abeedacd887f7770c198952fd5b9530d973e767" "sha256:c314c857e7cd47c856d2c5adff514ac2e6495f8b8e0f886a8a37e9305dfea0d8"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.4.2" "version": "==3.4.3"
}, },
"sphinx-rtd-theme": { "sphinx-rtd-theme": {
"hashes": [ "hashes": [
"sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d", "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5",
"sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82" "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.5.0" "version": "==0.5.1"
}, },
"sphinxcontrib-applehelp": { "sphinxcontrib-applehelp": {
"hashes": [ "hashes": [
@ -1656,11 +1380,20 @@
}, },
"tox": { "tox": {
"hashes": [ "hashes": [
"sha256:42ce19ce5dc2f6d6b1fdc5666c476e1f1e2897359b47e0aa3a5b774f335d57c2", "sha256:5efda30ad73e662c3844ac51ce1381bf28f61063773e06996aa8b6277133a7c0",
"sha256:4321052bfe28f9d85082341ca8e233e3ea901fdd14dab8a5d3fbd810269fbaf6" "sha256:8cccede64802e78aa6c69f81051b25f0706639d1cbbb34d9366ce00c70ee054f"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.20.1" "version": "==3.21.0"
},
"typing-extensions": {
"hashes": [
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
],
"markers": "python_version < '3.8'",
"version": "==3.7.4.3"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
@ -1672,11 +1405,19 @@
}, },
"virtualenv": { "virtualenv": {
"hashes": [ "hashes": [
"sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", "sha256:205a7577275dd0d9223c730dd498e21a8910600085c3dee97412b041fc4b853b",
"sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b" "sha256:7992b8de87e544a4ab55afc2240bf8388c4e3b5765d03784dad384bfdf9097ee"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.2.2" "version": "==20.3.0"
},
"zipp": {
"hashes": [
"sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
"sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
],
"markers": "python_version >= '3.6'",
"version": "==3.4.0"
} }
} }
} }

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.com/jonaswinkler/paperless-ng.svg?branch=master)](https://travis-ci.com/jonaswinkler/paperless-ng) ![ci](https://github.com/jonaswinkler/paperless-ng/workflows/ci/badge.svg)
[![Documentation Status](https://readthedocs.org/projects/paperless-ng/badge/?version=latest)](https://paperless-ng.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/paperless-ng/badge/?version=latest)](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
[![Gitter](https://badges.gitter.im/paperless-ng/community.svg)](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Gitter](https://badges.gitter.im/paperless-ng/community.svg)](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Docker Hub Pulls](https://img.shields.io/docker/pulls/jonaswinkler/paperless-ng.svg)](https://hub.docker.com/r/jonaswinkler/paperless-ng) [![Docker Hub Pulls](https://img.shields.io/docker/pulls/jonaswinkler/paperless-ng.svg)](https://hub.docker.com/r/jonaswinkler/paperless-ng)
@ -10,8 +10,6 @@
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the changelog in the documentation. Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the changelog in the documentation.
This project is still in development and some things may not work as expected.
# How it Works # How it Works
Paperless does not control your scanner, it only helps you deal with what your scanner produces. Paperless does not control your scanner, it only helps you deal with what your scanner produces.
@ -48,7 +46,7 @@ Here's what you get:
* Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless. * Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
* A task processor that processes documents in parallel and also tells you when something goes wrong. On modern multi core systems, consumption is blazing fast. * A task processor that processes documents in parallel and also tells you when something goes wrong. On modern multi core systems, consumption is blazing fast.
If you want to see some screenshots of paperless-ng in action, [some are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html). If you want to see some screenshots of paperless-ng in action, [some are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html). However, some parts of the UI have changed since I took these.
For a complete list of changes from paperless, check out the [changelog](https://paperless-ng.readthedocs.io/en/latest/changelog.html) For a complete list of changes from paperless, check out the [changelog](https://paperless-ng.readthedocs.io/en/latest/changelog.html)
@ -56,22 +54,7 @@ For a complete list of changes from paperless, check out the [changelog](https:/
- Make the front end nice (except mobile). - Make the front end nice (except mobile).
- Fix whatever bugs I and you find. - Fix whatever bugs I and you find.
- Make the documentation nice.
## Roadmap for versions beyond 1.0
These are things that I want to add to paperless eventually. They are sorted by priority.
- **More search.** The search backend is incredibly versatile and customizable. Searching is the most important feature of this project and thus, I want to implement things like:
- Group and limit search results by correspondent, show “more from this” links in the results.
- **Nested tags**. Organize tags in a hierarchical structure. This will combine the benefits of folders and tags in one coherent system.
- **Localization.** I won't translate paperless into any other languages except English and German, however, I'll add the necessary means so that anyone can translate paperless into their favorite language.
- **An interactive consumer** that shows its progress for documents it processes on the web page.
- With live updates and websockets. This already works on a dev branch, but requires a lot of new dependencies, which I'm not particularly happy about.
- Notifications when a document was added with buttons to open the new document right away.
- **Arbitrary tag colors**. Allow the selection of any color with a color picker.
- **More file types**. Possibly allow more file types to be processed by paperless, such as office .odt, .doc and .docx documents.
Apart from that, paperless is pretty much feature complete.
## On the chopping block. ## On the chopping block.
@ -88,7 +71,7 @@ These features will probably never make it into paperless, since paperless is me
# Getting started # Getting started
The recommended way to deploy paperless is docker-compose. Don't clone the repository, grab the latest release to get started instead. The dockerfiles archive contains just the docker files which will pull the image from docker hub. The source archive contains everything you need to build the docker image yourself (i.e. if you want to run on Raspberry Pi). The recommended way to deploy paperless is docker-compose. The files in the /docker/hub directory are configured to pull the image from Docker Hub.
Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started. Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started.
@ -102,6 +85,12 @@ Read the section about [migration](https://paperless-ng.readthedocs.io/en/latest
The documentation for Paperless-ng is available on [ReadTheDocs](https://paperless-ng.readthedocs.io/). The documentation for Paperless-ng is available on [ReadTheDocs](https://paperless-ng.readthedocs.io/).
# Translation
Paperless is currently available in English, German, Dutch and French. Translation is coordinated at transifex: https://www.transifex.com/paperless/paperless-ng
If you want to see paperless in your own language, request that language at transifex and you can start translating after I approve the language.
# Suggestions? Questions? Something not working? # Suggestions? Questions? Something not working?
Please open an issue and start a discussion about it! Please open an issue and start a discussion about it!

114
ansible/README.md Normal file

@ -0,0 +1,114 @@
Ansible Role: paperless-ng
==========================
Installs and configures paperless-ng EDMS on Debian/Ubuntu systems.
Requirements
------------
No special system requirements. Ansible 2.7 or newer is required.
Note that this role requires root access, so either run it in a playbook with a global `become: yes`, or invoke the role in your playbook like:
- hosts: all
roles:
- role: ansible
become: yes
Role Variables
--------------
Most configuration variables from paperless-ng itself are available and accept their respective arguments.
Every `PAPERLESS_*` configuration varaible is lowercased and instead prefixed with `paperlessng_*` in `defaults/main.yml`.
For a full listing including explainations and allowed values, see the current [documentation](https://paperless-ng.readthedocs.io/en/ng-0.9.14/configuration.html).
Additional variables available in this role are listed below, along with default values:
paperlessng_version: 0.9.14
The [release](https://github.com/jonaswinkler/paperless-ng/releases) archive version of paperless-ng to install.
paperlessng_redis_host: localhost
paperlessng_redis_port: 6379
Seperate configuration values that combine into `PAPERLESS_REDIS`.
paperlessng_db_type: sqlite
Database to use. Default is file-based SQLite.
paperlessng_db_host: localhost
paperlessng_db_port: 5432
paperlessng_db_name: paperlessng
paperlessng_db_user: paperlessng
paperlessng_db_pass: paperlessng
paperlessng_db_sslmode: prefer
Database configuration (only applicable if `paperlessng_db_type == 'postgresql'`).
paperlessng_directory: /opt/paperless-ng
Root directory paperless-ng is installed into.
paperlessng_virtualenv: "{{ paperlessng_directory }}/.venv"
Directory used for the virtual environment for paperless-ng.
paperlessng_ocr_languages:
- eng
List of OCR languages to install and configure (`apt search tesseract-ocr-*`).
paperlessng_use_jbig2enc: True
Whether to install and use [jbig2enc](https://github.com/agl/jbig2enc) for OCRmyPDF.
paperlessng_big2enc_lossy: False
Whether to use jbig2enc's lossy compression mode.
paperlessng_superuser_name: paperlessng
paperlessng_superuser_email: paperlessng@example.com
paperlessng_superuser_password: paperlessng
Credentials of the initial superuser in paperless-ng.
paperlessng_system_user: paperlessng
paperlessng_system_group: paperlessng
System user and group to run the paperless-ng services as (will be created if required).
paperlessng_listen_address: 127.0.0.1
paperlessng_listen_port: 8000
Address and port for the paperless-ng service to listen on.
Dependencies
------------
No ansible dependencies.
Example Playbook
----------------
`playbook.yml`:
- hosts: all
become: yes
vars_files:
- vars/main.yml
roles:
- ansible
`vars/main.yml`:
paperlessng_media_root: /mnt/media/smbshare
paperlessng_db_type: postgresql
paperlessng_db_pass: PLEASEPROVIDEASTRONGPASSWORDHERE
paperless_secret_key: AGAINPLEASECHANGETHISNOW
paperlessng_ocr_languages:
- eng
- deu

78
ansible/defaults/main.yml Normal file

@ -0,0 +1,78 @@
---
paperlessng_version: 0.9.14
# Required services
paperlessng_redis_host: localhost
paperlessng_redis_port: 6379
paperlessng_db_type: sqlite # or postgresql
# Below entries only apply for paperlessng_db_type=='postgresql'
paperlessng_db_host: localhost
paperlessng_db_port: 5432
paperlessng_db_name: paperlessng
paperlessng_db_user: paperlessng
paperlessng_db_pass: paperlessng
paperlessng_db_sslmode: prefer
# Paths and folders
paperlessng_directory: /opt/paperless-ng
paperlessng_consumption_dir: "{{ paperlessng_directory }}/consumption"
paperlessng_data_dir: "{{ paperlessng_directory }}/data"
paperlessng_media_root: "{{ paperlessng_directory }}/media"
paperlessng_static_dir: "{{ paperlessng_directory }}/static"
paperlessng_filename_format:
paperlessng_virtualenv: "{{ paperlessng_directory }}/.venv"
# Hosting & Security
paperless_secret_key: PLEASECHANGETHISFORTHELOVEOFGOD
paperless_allowed_hosts: "*"
paperless_cors_allowed_hosts: http://localhost:8000
paperless_force_script_name:
paperless_static_url: /static/
paperless_auto_login_username:
paperless_cookie_prefix: ""
paperless_enable_http_remote_user: False
# OCR settings
paperlessng_ocr_languages:
- eng
paperlessng_ocr_mode: skip
paperlessng_ocr_output_type: pdfa
paperlessng_ocr_pages: 0
paperlessng_ocr_image_dpi:
# see https://ocrmypdf.readthedocs.io/en/latest/api.html#ocrmypdf.ocr
paperlessng_ocr_user_args:
#- "deskew": True # https://github.com/jonaswinkler/paperless-ng/issues/231
- "optimize": 1
paperlessng_use_jbig2enc: True
paperlessng_big2enc_lossy: False
# Tika settings
paperlessng_tika_enabled: False
paperlessng_tika_endpoint: http://localhost:9998
paperlessng_tika_gotenberg_endpoint: http://localhost:3000
# Software tweaks
paperlessng_time_zone: Europe/Berlin
paperlessng_consumer_polling: 0
paperlessng_consumer_delete_duplicates: False
paperlessng_consumer_recursive: False
paperlessng_consumer_subdirs_as_tags: False
paperlessng_optimize_thumbnails: True
paperlessng_post_consume_script:
paperlessng_filename_date_order:
paperlessng_filename_parse_transforms:
paperlessng_thumbnail_font_name: /usr/share/fonts/liberation/LiberationSerif-Regular.ttf
paperlessng_ignore_dates: ""
# Superuser settings
paperlessng_superuser_name: paperlessng
paperlessng_superuser_email: paperlessng@example.com
paperlessng_superuser_password: paperlessng
# System user settings
paperlessng_system_user: paperlessng
paperlessng_system_group: paperlessng
# Webserver settings
paperlessng_listen_address: 127.0.0.1
paperlessng_listen_port: 8000

17
ansible/meta/main.yml Normal file

@ -0,0 +1,17 @@
dependencies: []
galaxy_info:
author: C0nsultant
description: Bare-metal deployment of paperless-ng DMS
license: license (GPLv3)
min_ansible_version: 2.7
platforms:
- name: Debian
versions:
- buster
- name: Ubuntu
versions:
- focal
galaxy_tags: [EDMS, django, python, web]

@ -0,0 +1,7 @@
---
- name: fresh installation
hosts: all
tasks:
- name: install paperless-ng with default parameters
include_role:
name: ansible

@ -0,0 +1,35 @@
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu_focal
image: jrei/systemd-ubuntu:20.04
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
- /run/lock
override_command: False
# ubuntu 18.04 bionic works except that
# the default redis configuration expects IPv6 which is not enabled in docker by default
# the default Python environment is configured for ASCII instead of UTF-8
# ubuntu 16.04 xenial only has Python 3.5 which is EOL and breaks multiple dependencies
- name: debian_buster
image: jrei/systemd-debian:10
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
- /run/lock
override_command: False
# debian 9 stretch only has Python 3.5 which is EOL and breaks multiple dependencies
provisioner:
name: ansible
verifier:
name: ansible

@ -0,0 +1,60 @@
---
- name: Verify
hosts: all
gather_facts: false
vars_files:
- ../../defaults/main.yml
tasks:
- name: check if webserver is up
uri:
url: http://localhost:8000
status_code: [200, 302]
return_content: yes
register: landingpage
failed_when: "'Sign in</button>' not in landingpage.content"
- name: check if document posting works
uri:
url: http://localhost:8000/api/documents/post_document/
method: POST
body_format: form-multipart
body:
document:
content: FOO
filename: document.txt
mime_type: text/plain
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: post_document
failed_when: "'OK' not in post_document.content"
- name: verify uploaded document has been accepted
uri:
url: http://localhost:8000/api/logs/
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: logs
failed_when: "'Consuming document.txt' not in logs.content"
# assumes txt consumption finished by now, might have to sleep a bit
- name: verify uploaded document has been consumed
uri:
url: http://localhost:8000/api/logs/
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: logs
failed_when: "'document consumption finished' not in logs.content"
- name: verify uploaded document is avaiable
uri:
url: http://localhost:8000/api/documents/1/
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: document
failed_when: "'Not found.' in document.content or 'FOO' not in document.content"

@ -0,0 +1,11 @@
---
- name: update previous release to newest release
hosts: all
tasks:
- name: set current version as installation target
set_fact:
paperlessng_version: 0.9.14
- name: update to newest paperless-ng release
include_role:
name: ansible

@ -0,0 +1,35 @@
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu_focal
image: jrei/systemd-ubuntu:20.04
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
- /run/lock
override_command: False
# ubuntu 18.04 bionic works except that
# the default redis configuration expects IPv6 which is not enabled in docker by default
# the default Python environment is configured for ASCII instead of UTF-8
# ubuntu 16.04 xenial only has Python 3.5 which is EOL and breaks multiple dependencies
- name: debian_buster
image: jrei/systemd-debian:10
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /tmp
- /run
- /run/lock
override_command: False
# debian 9 stretch only has Python 3.5 which is EOL and breaks multiple dependencies
provisioner:
name: ansible
verifier:
name: ansible

@ -0,0 +1,10 @@
- name: install previous release
hosts: all
tasks:
- name: set previous version as installation target
set_fact:
paperlessng_version: 0.9.13
- name: install previous paperless-ng release
include_role:
name: ansible

@ -0,0 +1,60 @@
---
- name: Verify
hosts: all
gather_facts: false
vars_files:
- ../../defaults/main.yml
tasks:
- name: check if webserver is up
uri:
url: http://localhost:8000
status_code: [200, 302]
return_content: yes
register: landingpage
failed_when: "'Sign in</button>' not in landingpage.content"
- name: check if document posting works
uri:
url: http://localhost:8000/api/documents/post_document/
method: POST
body_format: form-multipart
body:
document:
content: FOO
filename: document.txt
mime_type: text/plain
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: post_document
failed_when: "'OK' not in post_document.content"
- name: verify uploaded document has been accepted
uri:
url: http://localhost:8000/api/logs/
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: logs
failed_when: "'Consuming document.txt' not in logs.content"
# assumes txt consumption finished by now, might have to sleep a bit
- name: verify uploaded document has been consumed
uri:
url: http://localhost:8000/api/logs/
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: logs
failed_when: "'document consumption finished' not in logs.content"
- name: verify uploaded document is avaiable
uri:
url: http://localhost:8000/api/documents/1/
headers:
Authorization: 'Basic {{ (paperlessng_superuser_name + ":" + paperlessng_superuser_password) | b64encode }}'
return_content: yes
register: document
failed_when: "'Not found.' in document.content or 'FOO' not in document.content"

454
ansible/tasks/main.yml Normal file

@ -0,0 +1,454 @@
---
- name: verify operating system
fail:
msg: Sorry, only Debian and Ubuntu supported at the moment.
when: not(ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu')
- name: install base dependencies
apt:
update_cache: yes
pkg:
# paperless-ng
- python3-pip
- python3-dev
- fonts-liberation
- imagemagick
- optipng
- gnupg
- libpoppler-cpp-dev
- libpq-dev
- libmagic-dev
- mime-support
# OCRmyPDF
- unpaper
- ghostscript
- icc-profiles-free
- qpdf
- liblept5
- libxml2
- pngquant
- zlib1g
- tesseract-ocr
# dev
- sudo
- build-essential
- python3-setuptools
- python3-wheel
- python3-virtualenv
- name: install ocr languages
apt:
pkg: "{{ paperlessng_ocr_languages | map('regex_replace', '^(.*)$', 'tesseract-ocr-\\1') | list }}"
- name: set up notesalexp repository key (for jbig2enc)
apt_key:
url: https://notesalexp.org/debian/alexp_key.asc
state: present
when: paperlessng_use_jbig2enc
- name: set up notesalexp repository (for jbig2enc)
apt_repository:
repo: "deb https://notesalexp.org/debian/{{ ansible_distribution_release }}/ {{ ansible_distribution_release }} main"
state: present
when: paperlessng_use_jbig2enc
- name: set up notesalexp repository pinning
copy:
content: |
Package: *
Pin: release o=notesalexp.org
Pin-Priority: 1
Package: jbig2enc
Pin: release o=notesalexp.org
Pin-Priority: 500
dest: /etc/apt/preferences.d/notesalexp
when: paperlessng_use_jbig2enc
- name: install jbig2enc
apt:
pkg: jbig2enc
update_cache: yes
when: paperlessng_use_jbig2enc
- name: install redis
apt:
pkg: redis-server
when: paperlessng_redis_host == 'localhost' or paperlessng_redis_host == '127.0.0.1'
- name: enable redis
systemd:
name: redis-server
enabled: yes
masked: no
state: started
when: paperlessng_redis_host == 'localhost' or paperlessng_redis_host == '127.0.0.1'
- name: create paperless system group
group:
name: "{{ paperlessng_system_group }}"
- name: create paperless system user
user:
name: "{{ paperlessng_system_user }}"
groups:
- "{{ paperlessng_system_group }}"
shell: /usr/sbin/nologin
# GNUPG_HOME required due to paperless db.py
create_home: yes
- name: check for paperless-ng installation
command:
cmd: 'grep -Po "(?<=Paperless-ng )\d+\.\d+\.\d+" {{ paperlessng_directory }}/docs/changelog.html'
changed_when: '"No such file or directory" in paperlessng_current_version.stderr or paperlessng_current_version.stdout != paperlessng_version | string'
failed_when: false
ignore_errors: yes
register: paperlessng_current_version
- name: register current state
set_fact:
fresh_installation: '{{ "No such file or directory" in paperlessng_current_version.stderr }}'
update_installation: '{{ "No such file or directory" not in paperlessng_current_version.stderr and paperlessng_current_version.stdout != paperlessng_version | string }}'
reconfigure_only: '{{ paperlessng_current_version.stdout == paperlessng_version | string }}'
- name: backup current paperless-ng installation
copy:
src: "{{ paperlessng_directory }}"
remote_src: yes
dest: "{{ paperlessng_directory }}-{{ ansible_date_time.iso8601 }}/"
when: update_installation
- name: remove current paperless sources
file:
path: "{{ paperlessng_directory }}/{{ item }}"
state: absent
with_items:
- docker
- docs
- scripts
- src
- static
when: update_installation
- name: create temporary directory
tempfile:
state: directory
register: tempdir
when: not reconfigure_only
- name: extract paperless-ng
unarchive:
src: "https://github.com/jonaswinkler/paperless-ng/releases/download/ng-{{ paperlessng_version }}/paperless-ng-{{ paperlessng_version }}.tar.xz"
remote_src: yes
dest: "{{ tempdir.path }}"
when: not reconfigure_only
- name: change owner and permissions of paperless-ng
command:
cmd: "{{ item }}"
warn: false
with_items:
- "chown -R {{ paperlessng_system_user }}:{{ paperlessng_system_group }} {{ tempdir.path }}"
- "find {{ tempdir.path }} -type d -exec chmod 0750 {} ;"
- "find {{ tempdir.path }} -type f -exec chmod 0640 {} ;"
when: not reconfigure_only
- name: move paperless-ng
command:
cmd: "cp -a {{ tempdir.path }}/paperless-ng/. {{ paperlessng_directory }}"
when: not reconfigure_only
- name: remove temporary directory
file:
path: "{{ tempdir.path }}"
state: absent
when: not reconfigure_only
- name: create paperless-ng directories and set permissions
file:
path: "{{ item }}"
state: directory
owner: "{{ paperlessng_system_user }}"
group: "{{ paperlessng_system_group }}"
mode: "750"
with_items:
- "{{ paperlessng_directory }}"
- "{{ paperlessng_consumption_dir }}"
- "{{ paperlessng_data_dir }}"
- "{{ paperlessng_media_root }}"
- "{{ paperlessng_static_dir }}"
- name: rename initial config
command:
cmd: "mv {{ paperlessng_directory }}/paperless.conf {{ paperlessng_directory }}/paperless.conf.template"
removes: "{{ paperlessng_directory }}/paperless.conf"
- name: configure paperless-ng
lineinfile:
path: "{{ paperlessng_directory }}/paperless.conf.template"
regexp: "^#?{{ item.regexp }}="
line: "{{ item.line }}"
with_items:
# Required services
- regexp: PAPERLESS_REDIS
line: "PAPERLESS_REDIS=redis://{{ paperlessng_redis_host }}:{{ paperlessng_redis_port }}"
# Paths and folders
- regexp: PAPERLESS_CONSUMPTION_DIR
line: "PAPERLESS_CONSUMPTION_DIR={{ paperlessng_consumption_dir }}"
- regexp: PAPERLESS_DATA_DIR
line: "PAPERLESS_DATA_DIR={{ paperlessng_data_dir }}"
- regexp: PAPERLESS_MEDIA_ROOT
line: "PAPERLESS_MEDIA_ROOT={{ paperlessng_media_root }}"
- regexp: PAPERLESS_STATICDIR
line: "PAPERLESS_STATICDIR={{ paperlessng_static_dir }}"
- regexp: PAPERLESS_FILENAME_FORMAT
line: "PAPERLESS_FILENAME_FORMAT={{ paperlessng_filename_format }}"
# Hosting & Security
- regexp: PAPERLESS_SECRET_KEY
line: "PAPERLESS_SECRET_KEY={{ paperless_secret_key }}"
- regexp: PAPERLESS_ALLOWED_HOSTS
line: "PAPERLESS_ALLOWED_HOSTS={{ paperless_allowed_hosts }}"
- regexp: PAPERLESS_CORS_ALLOWED_HOSTS
line: "PAPERLESS_CORS_ALLOWED_HOSTS={{ paperless_cors_allowed_hosts }}"
- regexp: PAPERLESS_FORCE_SCRIPT_NAME
line: "PAPERLESS_FORCE_SCRIPT_NAME={{ paperless_force_script_name }}"
- regexp: PAPERLESS_STATIC_URL
line: "PAPERLESS_STATIC_URL={{ paperless_static_url }}"
- regexp: PAPERLESS_AUTO_LOGIN_USERNAME
line: "PAPERLESS_AUTO_LOGIN_USERNAME={{ paperless_auto_login_username }}"
- regexp: PAPERLESS_COOKIE_PREFIX
line: "PAPERLESS_COOKIE_PREFIX={{ paperless_cookie_prefix }}"
- regexp: PAPERLESS_ENABLE_HTTP_REMOTE_USER
line: "PAPERLESS_ENABLE_HTTP_REMOTE_USER={{ paperless_enable_http_remote_user }}"
# OCR settings
- regexp: PAPERLESS_OCR_LANGUAGE
line: "PAPERLESS_OCR_LANGUAGE={{ paperlessng_ocr_languages | join('+') }}"
- regexp: PAPERLESS_OCR_MODE
line: "PAPERLESS_OCR_MODE={{ paperlessng_ocr_mode }}"
- regexp: PAPERLESS_OCR_OUTPUT_TYPE
line: "PAPERLESS_OCR_OUTPUT_TYPE={{ paperlessng_ocr_output_type }}"
- regexp: PAPERLESS_OCR_PAGES
line: "PAPERLESS_OCR_PAGES={{ paperlessng_ocr_pages }}"
- regexp: PAPERLESS_OCR_IMAGE_DPI
line: "PAPERLESS_OCR_IMAGE_DPI={{ paperlessng_ocr_image_dpi }}"
- regexp: PAPERLESS_OCR_USER_ARGS
line: "PAPERLESS_OCR_USER_ARGS={{ paperlessng_ocr_user_args | combine({'jbig2_lossy': true} if paperlessng_big2enc_lossy else {}) | to_json }}"
# Tika settings
- regexp: PAPERLESS_TIKA_ENABLED
line: "PAPERLESS_TIKA_ENABLED={{ paperlessng_tika_enabled }}"
- regexp: PAPERLESS_TIKA_ENDPOINT
line: "PAPERLESS_TIKA_ENDPOINT={{ paperlessng_tika_endpoint }}"
- regexp: PAPERLESS_TIKA_GOTENBERG_ENDPOINT
line: "PAPERLESS_TIKA_GOTENBERG_ENDPOINT={{ paperlessng_tika_endpoint }}"
# Software tweaks
- regexp: PAPERLESS_TIME_ZONE
line: "PAPERLESS_TIME_ZONE={{ paperlessng_time_zone }}"
- regexp: PAPERLESS_CONSUMER_POLLING
line: "PAPERLESS_CONSUMER_POLLING={{ paperlessng_consumer_polling }}"
- regexp: PAPERLESS_CONSUMER_DELETE_DUPLICATES
line: "PAPERLESS_CONSUMER_DELETE_DUPLICATES={{ paperlessng_consumer_delete_duplicates }}"
- regexp: PAPERLESS_CONSUMER_RECURSIVE
line: "PAPERLESS_CONSUMER_RECURSIVE={{ paperlessng_consumer_recursive }}"
- regexp: PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS
line: "PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS={{ paperlessng_consumer_subdirs_as_tags }}"
- regexp: PAPERLESS_OPTIMIZE_THUMBNAILS
line: "PAPERLESS_OPTIMIZE_THUMBNAILS={{ paperlessng_optimize_thumbnails }}"
- regexp: PAPERLESS_POST_CONSUME_SCRIPT
line: "PAPERLESS_POST_CONSUME_SCRIPT={{ paperlessng_post_consume_script }}"
- regexp: PAPERLESS_FILENAME_DATE_ORDER
line: "PAPERLESS_FILENAME_DATE_ORDER={{ paperlessng_filename_date_order }}"
- regexp: PAPERLESS_THUMBNAIL_FONT_NAME
line: "PAPERLESS_THUMBNAIL_FONT_NAME={{ paperlessng_thumbnail_font_name }}"
- regexp: PAPERLESS_IGNORE_DATES
line: "PAPERLESS_IGNORE_DATES={{ paperlessng_ignore_dates }}"
no_log: yes
- name: configure paperless-ng database [sqlite]
lineinfile:
path: "{{ paperlessng_directory }}/paperless.conf.template"
regexp: "^#?PAPERLESS_DBHOST=(.*)$"
line: '#PAPERLESS_DBHOST=\1'
backrefs: yes
when: paperlessng_db_type == 'sqlite'
- name: configure paperless-ng database [postgresql]
lineinfile:
path: "{{ paperlessng_directory }}/paperless.conf.template"
regexp: "^#?{{ item.regexp }}="
line: "{{ item.line }}"
with_items:
- regexp: PAPERLESS_DBHOST
line: "PAPERLESS_DBHOST={{ paperlessng_db_host }}"
- regexp: PAPERLESS_DBPORT
line: "PAPERLESS_DBPORT={{ paperlessng_db_port }}"
- regexp: PAPERLESS_DBNAME
line: "PAPERLESS_DBNAME={{ paperlessng_db_name }}"
- regexp: PAPERLESS_DBUSER
line: "PAPERLESS_DBUSER={{ paperlessng_db_user }}"
- regexp: PAPERLESS_DBPASS
line: "PAPERLESS_DBPASS={{ paperlessng_db_pass }}"
- regexp: PAPERLESS_DBSSLMODE
line: "PAPERLESS_DBSSLMODE={{ paperlessng_db_sslmode }}"
when: paperlessng_db_type == 'postgresql'
no_log: yes
- name: deploy paperless-ng configuration
copy:
src: "{{ paperlessng_directory }}/paperless.conf.template"
remote_src: yes
dest: /etc/paperless.conf
owner: root
group: root
mode: "0644"
register: configuration
- name: create paperlessng venv
become: yes
become_user: "{{ paperlessng_system_user }}"
command:
cmd: "python3 -m virtualenv {{ paperlessng_virtualenv }} -p /usr/bin/python3"
creates: "{{ paperlessng_virtualenv }}"
register: venv
- name: install paperlessng requirements
become: yes
become_user: "{{ paperlessng_system_user }}"
pip:
requirements: "{{ paperlessng_directory }}/requirements.txt"
executable: "{{ paperlessng_virtualenv }}/bin/pip3"
extra_args: --upgrade
when: not reconfigure_only
- name: migrate database schema
become: yes
become_user: "{{ paperlessng_system_user }}"
command: "{{ paperlessng_virtualenv }}/bin/python3 {{ paperlessng_directory }}/src/manage.py migrate"
register: database_schema
changed_when: '"No migrations to apply." not in database_schema.stdout'
when: not reconfigure_only
- name: configure paperless superuser
become: yes
become_user: "{{ paperlessng_system_user }}"
# "manage.py createsuperuser" only works on interactive TTYs
vars:
creation_script: |
from django.contrib.auth.models import User
from django.contrib.auth.hashers import get_hasher
if User.objects.filter(username='{{ paperlessng_superuser_name }}').exists():
user = User.objects.get(username='{{ paperlessng_superuser_name }}')
old = user.__dict__.copy()
user.is_superuser = True
user.email = '{{ paperlessng_superuser_email }}'
user.set_password('{{ paperlessng_superuser_password }}')
user.save()
new = user.__dict__
algorithm, iterations, old_salt, old_hash = old['password'].split('$')
new_password_old_salt = get_hasher(algorithm).encode(password='{{ paperlessng_superuser_password }}', salt=old_salt, iterations=int(iterations))
_, _, _, new_hash = new_password_old_salt.split('$')
if not (old_hash == new_hash and old['is_superuser'] == new['is_superuser'] and old['email'] == new['email']):
print('changed')
else:
User.objects.create_superuser('{{ paperlessng_superuser_name }}', '{{ paperlessng_superuser_email }}', '{{ paperlessng_superuser_password }}')
print('changed')
command: '{{ paperlessng_virtualenv }}/bin/python3 {{ paperlessng_directory }}/src/manage.py shell -c "{{ creation_script }}"'
register: superuser
changed_when: superuser.stdout == 'changed'
no_log: yes
- name: set ownership and permissions on paperlessng venv
file:
path: "{{ paperlessng_virtualenv }}"
state: directory
recurse: yes
owner: "{{ paperlessng_system_user }}"
group: "{{ paperlessng_system_group }}"
mode: g-w,o-rwx
when: venv.changed or not reconfigure_only
- name: configure ghostscript for PDF
lineinfile:
path: /etc/ImageMagick-6/policy.xml
regexp: '(\s+)<policy domain="coder" rights=".*" pattern="PDF" />'
line: '\1<policy domain="coder" rights="read|write" pattern="PDF" />'
backrefs: yes
- name: configure systemd services
ini_file:
path: "{{ paperlessng_directory }}/scripts/{{ item[0] }}"
section: "Service"
option: "{{ item[1].option }}"
value: "{{ item[1].value }}"
with_nested:
- [
paperless-consumer.service,
paperless-scheduler.service,
paperless-webserver.service,
]
- [
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html
{ option: "User", value: "{{ paperlessng_system_user }}" },
{ option: "Group", value: "{{ paperlessng_system_group }}" },
{ option: "WorkingDirectory", value: "{{ paperlessng_directory }}/src", },
{ option: "ProtectSystem", value: "full" },
{ option: "NoNewPrivileges", value: "true" },
{ option: "PrivateUsers", value: "true" },
{ option: "PrivateDevices", value: "true" },
]
- name: configure paperless-consumer service
ini_file:
path: "{{ paperlessng_directory }}/scripts/paperless-consumer.service"
section: "Service"
option: "ExecStart"
value: "{{ paperlessng_virtualenv }}/bin/python3 manage.py document_consumer"
- name: configure paperless-scheduler service
ini_file:
path: "{{ paperlessng_directory }}/scripts/paperless-scheduler.service"
section: "Service"
option: "ExecStart"
value: "{{ paperlessng_virtualenv }}/bin/python3 manage.py qcluster"
- name: configure paperless-webserver service
ini_file:
path: "{{ paperlessng_directory }}/scripts/paperless-webserver.service"
section: "Service"
option: "ExecStart"
value: "{{ paperlessng_virtualenv }}/bin/gunicorn paperless.wsgi -w 2 -b {{ paperlessng_listen_address }}:{{ paperlessng_listen_port }}"
- name: copy systemd services
copy:
src: "{{ paperlessng_directory }}/scripts/{{ item }}"
remote_src: yes
dest: "/etc/systemd/system/{{ item }}"
with_items:
- paperless-consumer.service
- paperless-scheduler.service
- paperless-webserver.service
register: paperless_services
- name: reload systemd daemon
systemd:
name: "{{ item }}"
state: restarted
daemon_reload: yes
with_items:
- paperless-consumer
- paperless-scheduler
- paperless-webserver
when: paperless_services.changed or configuration.changed
- name: enable paperlessng services
systemd:
name: "{{ item }}"
enabled: yes
masked: no
state: started
with_items:
- paperless-consumer
- paperless-scheduler
- paperless-webserver

7
compile-frontend.sh Executable file

@ -0,0 +1,7 @@
#!/bin/bash
set -e
cd src-ui
npm install
./node_modules/.bin/ng build --prod

5
crowdin.yml Normal file

@ -0,0 +1,5 @@
files:
- source: /src/locale/en-us/LC_MESSAGES/django.po
translation: /src/locale/%two_letters_code%/LC_MESSAGES/django.po
- source: /src-ui/messages.xlf
translation: /src-ui/src/locale/messages.%two_letters_code%.xlf

1
docker/compose/.env Normal file

@ -0,0 +1 @@
COMPOSE_PROJECT_NAME=paperless

@ -7,7 +7,7 @@
# Additional languages to install for text recognition, separated by a # Additional languages to install for text recognition, separated by a
# whitespace. Note that this is # whitespace. Note that this is
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the # different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
# default language used when guessing the language from the OCR output. # language used for OCR.
# The container installs English, German, Italian, Spanish and French by # The container installs English, German, Italian, Spanish and French by
# default. # default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster # See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster

@ -0,0 +1,90 @@
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
# - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, Power Point and their LibreOffice counter-
# parts.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
version: "3.4"
services:
broker:
image: redis:6.0
restart: unless-stopped
db:
image: postgres:13
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: jonaswinkler/paperless-ng:latest
restart: unless-stopped
depends_on:
- db
- broker
- gotenberg
- tika
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
volumes:
data:
media:
pgdata:

@ -0,0 +1,72 @@
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
version: "3.4"
services:
broker:
image: redis:6.0
restart: unless-stopped
db:
image: postgres:13
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: jonaswinkler/paperless-ng:latest
restart: unless-stopped
depends_on:
- db
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
data:
media:
pgdata:

@ -0,0 +1,78 @@
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# In addition to that, this docker-compose file adds the following optional
# configurations:
#
# - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, Power Point and their LibreOffice counter-
# parts.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
version: "3.4"
services:
broker:
image: redis:6.0
restart: unless-stopped
webserver:
image: jonaswinkler/paperless-ng:latest
restart: unless-stopped
depends_on:
- broker
- gotenberg
- tika
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
volumes:
data:
media:

@ -0,0 +1,56 @@
# docker-compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker-compose pull'.
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
# - Run 'docker-compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
version: "3.4"
services:
broker:
image: redis:6.0
restart: unless-stopped
webserver:
image: jonaswinkler/paperless-ng:latest
restart: unless-stopped
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
volumes:
data:
media:

@ -62,6 +62,7 @@ migrations() {
# simultaneously. This also ensures that the db is ready when the command # simultaneously. This also ensures that the db is ready when the command
# of the current container starts. # of the current container starts.
flock 200 flock 200
echo "Apply database migrations..."
sudo -HEu paperless python3 manage.py migrate sudo -HEu paperless python3 manage.py migrate
) 200>/usr/src/paperless/data/migration_lock ) 200>/usr/src/paperless/data/migration_lock
@ -78,13 +79,19 @@ initialize() {
fi fi
done done
echo "creating directory /tmp/paperless"
mkdir -p /tmp/paperless
chown -R paperless:paperless ../ chown -R paperless:paperless ../
chown -R paperless:paperless /tmp/paperless
migrations migrations
} }
install_languages() { install_languages() {
echo "Installing languages..."
local langs="$1" local langs="$1"
read -ra langs <<<"$langs" read -ra langs <<<"$langs"
@ -119,6 +126,8 @@ install_languages() {
done done
} }
echo "Paperless-ng docker container starting..."
# Install additional languages if specified # Install additional languages if specified
if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then
install_languages "$PAPERLESS_OCR_LANGUAGES" install_languages "$PAPERLESS_OCR_LANGUAGES"
@ -127,8 +136,10 @@ fi
initialize initialize
if [[ "$1" != "/"* ]]; then if [[ "$1" != "/"* ]]; then
echo Executing management command "$@"
exec sudo -HEu paperless python3 manage.py "$@" exec sudo -HEu paperless python3 manage.py "$@"
else else
echo Executing "$@"
exec "$@" exec "$@"
fi fi

@ -1,44 +0,0 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
db:
image: postgres:13
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: jonaswinkler/paperless-ng:0.9.11
restart: always
depends_on:
- db
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
data:
media:
pgdata:

@ -1,31 +0,0 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
webserver:
image: jonaswinkler/paperless-ng:0.9.11
restart: always
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
volumes:
data:
media:

@ -1,43 +0,0 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
webserver:
image: jonaswinkler/paperless-ng:0.9.9
restart: always
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
volumes:
data:
media:

@ -1,44 +0,0 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
db:
image: postgres:13
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
build: .
restart: always
depends_on:
- db
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
data:
media:
pgdata:

@ -1,31 +0,0 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
webserver:
build: .
restart: always
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
volumes:
data:
media:

@ -1,43 +0,0 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
webserver:
build: .
restart: always
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
volumes:
data:
media:

@ -20,6 +20,8 @@ Options available to any installation of paperless:
metadata to a specific folder. You may import your documents into a metadata to a specific folder. You may import your documents into a
fresh instance of paperless again or store your documents in another fresh instance of paperless again or store your documents in another
DMS with this export. DMS with this export.
* The document exporter is also able to update an already existing export.
Therefore, incremental backups with ``rsync`` are entirely possible.
Options available to docker installations: Options available to docker installations:
@ -48,16 +50,16 @@ Options available to bare-metal and non-docker installations:
Restoring Restoring
========= =========
.. _administration-updating: .. _administration-updating:
Updating paperless Updating Paperless
################## ##################
Docker Route
============
If a new release of paperless-ng is available, upgrading depends on how you If a new release of paperless-ng is available, upgrading depends on how you
installed paperless-ng in the first place. The releases are available at installed paperless-ng in the first place. The releases are available at the
`release page <https://github.com/jonaswinkler/paperless-ng/releases>`_. `release page <https://github.com/jonaswinkler/paperless-ng/releases>`_.
First of all, ensure that paperless is stopped. First of all, ensure that paperless is stopped.
@ -69,58 +71,54 @@ First of all, ensure that paperless is stopped.
After that, :ref:`make a backup <administration-backup>`. After that, :ref:`make a backup <administration-backup>`.
A. If you used the dockerfiles archive, simply download the files of the new release, A. If you pull the image from the docker hub, all you need to do is:
adjust the settings in the files (i.e., the path to your consumption directory),
and replace your existing docker-compose files. Then start paperless as usual,
which will pull the new image, and update your database, if necessary:
.. code:: shell-session .. code:: shell-session
$ cd /path/to/paperless $ docker-compose pull
$ docker-compose up $ docker-compose up
If you see everything working, you can start paperless-ng with "-d" to have it The docker-compose files refer to the ``latest`` version, which is always the latest
run in the background. stable release.
.. hint:: B. If you built the image yourself, do the following:
The released docker-compose files specify exact versions to be pulled from the hub.
This is to ensure that if the docker-compose files should change at some point
(i.e., services updates/configured differently), you wont run into trouble due to
docker pulling the ``latest`` image and running it in an older environment.
B. If you built the image yourself, grab the new archive and replace your current
paperless folder with the new contents.
After that, make the necessary adjustments to the docker-compose.yml (i.e.,
adjust your consumption directory).
Build and start the new image with:
.. code:: shell-session .. code:: shell-session
$ cd /path/to/paperless $ git pull
$ ./compile-frontend.sh
$ docker-compose build $ docker-compose build
$ docker-compose up $ docker-compose up
If you see everything working, you can start paperless-ng with "-d" to have it Running ``docker-compose up`` will also apply any new database migrations.
run in the background. If you see everything working, press CTRL+C once to gracefully stop paperless.
Then you can start paperless-ng with ``-d`` to have it run in the background.
.. hint:: .. note::
You can usually keep your ``docker-compose.env`` file, since this file will In version 0.9.14, the update process was changed. In 0.9.13 and earlier, the
never include mandatory configuration options. However, it is worth checking docker-compose files specified exact versions and pull won't automatically
out the new version of this file, since it might have new recommendations update to newer versions. In order to enable updates as described above, either
on what to configure. get the new ``docker-compose.yml`` file from `here <https://github.com/jonaswinkler/paperless-ng/tree/master/docker/compose>`_
or edit the ``docker-compose.yml`` file, find the line that says
.. code::
image: jonaswinkler/paperless-ng:0.9.x
and replace the version with ``latest``:
Updating paperless without docker .. code::
=================================
image: jonaswinkler/paperless-ng:latest
Bare Metal Route
================
After grabbing the new release and unpacking the contents, do the following: After grabbing the new release and unpacking the contents, do the following:
1. Update dependencies. New paperless version may require additional 1. Update dependencies. New paperless version may require additional
dependencies. The dependencies required are listed in the section about dependencies. The dependencies required are listed in the section about
:ref:`bare metal installations <setup-bare_metal>`. :ref:`bare metal installations <setup-bare_metal>`.
2. Update python requirements. If you use Pipenv, this is done with the following steps. 2. Update python requirements. If you use Pipenv, this is done with the following steps.
@ -135,27 +133,52 @@ After grabbing the new release and unpacking the contents, do the following:
This creates a new virtual environment (or uses your existing environment) This creates a new virtual environment (or uses your existing environment)
and installs all dependencies into it. and installs all dependencies into it.
3. Collect static files. You can also use the included ``requirements.txt`` file instead and create the virtual
environment yourself. This file includes exactly the same dependencies.
.. code:: shell-session 3. Migrate the database.
$ cd src
$ pipenv run python3 manage.py collectstatic --clear
4. Migrate the database.
.. code:: shell-session .. code:: shell-session
$ cd src $ cd src
$ pipenv run python3 manage.py migrate $ pipenv run python3 manage.py migrate
5. Update translation files. This might not actually do anything. Not every new paperless version comes with new
database migrations.
Ansible Route
=============
Most of the update process is automated when using the ansible role.
1. Backup your defined role variables file outside the paperless source-tree:
.. code:: shell-session .. code:: shell-session
$ cd src $ cp ansible/vars.yml ~/vars.yml.old
$ pipenv run python3 manage.py compilemessages
2. Pull the release tag you want to update to:
.. code:: shell-session
$ git fetch --all
$ git checkout ng-0.9.14
3. Update the role variable definitions ``ansible/vars.yml`` (where appropriate).
4. Run the ansible playbook you created created during :ref:`installation <setup-ansible>` again:
.. note::
When ansible detects that an update run is in progress, it backs up the entire ``paperlessng_directory`` to ``paperlessng_directory-TIMESTAMP``.
Updates can be rolled back by simply moving the timestamped folder back to the original location.
If the update succeeds and you want to continue using the new release, please don't forget to delete the backup folder.
.. code:: shell-session
$ ansible-playbook playbook.yml
Management utilities Management utilities
#################### ####################
@ -189,8 +212,13 @@ backup or migration to another DMS.
.. code:: .. code::
document_exporter target document_exporter target [-c] [-f] [-d]
optional arguments:
-c, --compare-checksums
-f, --use-filename-format
-d, --delete
``target`` is a folder to which the data gets written. This includes documents, ``target`` is a folder to which the data gets written. This includes documents,
thumbnails and a ``manifest.json`` file. The manifest contains all metadata from thumbnails and a ``manifest.json`` file. The manifest contains all metadata from
the database (correspondents, tags, etc). the database (correspondents, tags, etc).
@ -199,6 +227,24 @@ When you use the provided docker compose script, specify ``../export`` as the
target. This path inside the container is automatically mounted on your host on target. This path inside the container is automatically mounted on your host on
the folder ``export``. the folder ``export``.
If the target directory already exists and contains files, paperless will assume
that the contents of the export directory are a previous export and will attempt
to update the previous export. Paperless will only export changed and added files.
Paperless determines whether a file has changed by inspecting the file attributes
"date/time modified" and "size". If that does not work out for you, specify
``--compare-checksums`` and paperless will attempt to compare file checksums instead.
This is slower.
Paperless will not remove any existing files in the export directory. If you want
paperless to also remove files that do not belong to the current export such as files
from deleted documents, specify ``--delete``. Be careful when pointing paperless to
a directory that already contains other files.
The filenames generated by this command follow the format
``[date created] [correspondent] [title].[extension]``.
If you want paperless to use ``PAPERLESS_FILENAME_FORMAT`` for exported filenames
instead, specify ``--use-filename-format``.
.. _utilities-importer: .. _utilities-importer:
@ -393,7 +439,7 @@ Documents can be stored in Paperless using GnuPG encryption.
Furthermore, the entire text content of the documents is stored plain in the Furthermore, the entire text content of the documents is stored plain in the
database, even if your documents are encrypted. Filenames are not encrypted as database, even if your documents are encrypted. Filenames are not encrypted as
well. well.
Also, the web server provides transparent access to your encrypted documents. Also, the web server provides transparent access to your encrypted documents.
Consider running paperless on an encrypted filesystem instead, which will then Consider running paperless on an encrypted filesystem instead, which will then
@ -418,4 +464,4 @@ Basic usage to disable encryption of your document store:
decrypt_documents [--passphrase SECR3TP4SSPHRA$E] decrypt_documents [--passphrase SECR3TP4SSPHRA$E]
.. _Pipenv: https://pipenv.pypa.io/en/latest/ .. _Pipenv: https://pipenv.pypa.io/en/latest/

@ -5,6 +5,83 @@
Changelog Changelog
********* *********
paperless-ng 1.0.0
##################
Nothing special about this release, but since there are relatively few bug reports coming in, I think that this is reasonably stable.
* Document export
* The document exporter has been rewritten to support updating an already existing export in place.
This enables incremental backups with ``rsync``.
* The document exporter supports naming exported files according to ``PAPERLESS_FILENAME_FORMAT``.
* The document exporter locks the media directory and the database during execution to ensure that
the resulting export is consistent.
* See the :ref:`updated documentation <utilities-exporter>` for more details.
* Other changes and additions
* Added a language selector to the settings.
* Added date format options to the settings.
* Range selection with shift clicking is now possible in the document list.
* Filtering correspondent, type and tag management pages by name.
* Focus "Name" field in dialogs by default.
paperless-ng 0.9.14
###################
Starting with this version, releases are getting built automatically. This release also comes with changes on how to install and
update paperless.
* Paperless now uses GitHub Actions to make releases and build docker images.
* Docker images are available for amd64, armhf, and aarch64.
* When you pull an image from Docker Hub, Docker will automatically select the correct image for you.
* Changes to docker installations and updates
* The ``-dockerfiles.tar.xz`` release archive is gone. Instead, simply grab the docker files from ``/docker/compose`` in the repository
if you wish to install paperless by pulling from the hub.
* The docker compose files in ``/docker/compose`` were changed to always use the ``latest`` version automatically. In order to do further
updates, simply do a ``docker-compose pull``. The documentation has been updated.
* The docker compose files were changed to restart paperless on system boot only if it was running before shutdown.
* Documentation of the docker-compose files about what they do.
* Changes to bare metal installations and updates
* The release archive is built exactly like before. However, the release now comes with already compiled translation messages and
collected static files. Therefore, the update steps ``compilemessages`` and ``collectstatic`` are now obsolete.
* Other changes
* A new configuration option ``PAPERLESS_IGNORE_DATES`` was added by `jayme-github`_. This can be used to instruct paperless to ignore
certain dates (such as your date of birth) when guessing the date from the document content. This was actually introduced in 0.9.12,
I just forgot to mention it in the changelog.
* The filter drop downs now display selected entries on top of all other entries.
* The PostgreSQL client now supports setting an explicit ``sslmode`` to force encryption of the connection to PostgreSQL.
* The docker images now come with ``jbig2enc``, which is a lossless image encoder for PDF documents and decreases the size of certain
PDF/A documents.
* When using any of the manual matching algorithms, paperless now logs messages about when and why these matching algorithms matched.
* The default settings for parallelization in paperless were adjusted to always leave one CPU core free.
* Added an option to the frontend to choose which method to use for displaying PDF documents.
* Fixes
* An issue with the tika parser not picking up files from the consumption directory was fixed.
* A couple changes to the dark mode and fixes to several other layout issues.
* An issue with the drop downs for correspondents, tags and types not properly supporting filtering with special characters was fixed.
* Fixed an issue with filenames of downloaded files: Dates where off by one day due to timezone issues.
* Searching will continue to work even when the index returns non-existing documents. This resulted in "Document does not exist" errors
before. Instead, a warning is logged, indicating the issue.
* An issue with the consumer crashing when invalid regular expression were used was fixed.
paperless-ng 0.9.13
###################
* Fixed an issue with Paperless not starting due to the new Tika integration when ``USERMAP_UID`` and ``USERMAP_GID`` was used
in the ``docker-compose.env`` file.
paperless-ng 0.9.12 paperless-ng 0.9.12
################### ###################
@ -17,6 +94,7 @@ paperless-ng 0.9.12
* See the :ref:`configuration<configuration-tika>` on how to enable this feature. This feature requires two additional services * See the :ref:`configuration<configuration-tika>` on how to enable this feature. This feature requires two additional services
(one for parsing Office documents and metadata extraction and another for converting Office documents to PDF), and is therefore (one for parsing Office documents and metadata extraction and another for converting Office documents to PDF), and is therefore
not enabled on default installations. not enabled on default installations.
* As with all other documents, paperless converts Office documents to PDF and stores both the original as well as the archived PDF.
* Dark mode * Dark mode
@ -32,6 +110,12 @@ paperless-ng 0.9.12
indicators and clearer error messages about what's wrong. indicators and clearer error messages about what's wrong.
* Paperless disables buttons with network actions (such as save and delete) when a network action is active. This indicates that * Paperless disables buttons with network actions (such as save and delete) when a network action is active. This indicates that
something is happening and prevents double clicking. something is happening and prevents double clicking.
* When using "Save & next", the title field is focussed automatically to better support keyboard editing.
* E-Mail: Added filter rule parameters to allow inline attachments (watch out for mails with inlined images!) and attachment filename filters
with wildcards.
* Support for remote user authentication thanks to `Michael Shamoon`_. This is useful for hiding Paperless behind single sign on applications
such as `authelia <https://www.authelia.com/>`_.
* "Clear filters" has been renamed to "Reset filters" and now correctly restores the default filters on saved views. Thanks to `Michael Shamoon`_
* Fixes * Fixes
@ -55,7 +139,7 @@ paperless-ng 0.9.10
* There are some configuration options in the settings to alter the behavior. * There are some configuration options in the settings to alter the behavior.
* Other changes and additions * Other changes and additions
* Thanks to `zjean`_, paperless now publishes a webmanifest, which is useful for adding the application to home screens on mobile devices. * Thanks to `zjean`_, paperless now publishes a webmanifest, which is useful for adding the application to home screens on mobile devices.
* The Paperless-ng logo now navigates to the dashboard. * The Paperless-ng logo now navigates to the dashboard.
* Filter for documents that don't have any correspondents, types or tags assigned. * Filter for documents that don't have any correspondents, types or tags assigned.
@ -75,7 +159,7 @@ paperless-ng 0.9.10
The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and
caused the search to return messages about missing documents when searching. Further bulk operations will properly update caused the search to return messages about missing documents when searching. Further bulk operations will properly update
the index. the index.
However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index
by :ref:`running the management command document_index with the argument reindex <administration-index>`. by :ref:`running the management command document_index with the argument reindex <administration-index>`.
@ -130,12 +214,12 @@ paperless-ng 0.9.7
* Thanks to the hard work of `Michael Shamoon`_, paperless now comes with a much more streamlined UI for * Thanks to the hard work of `Michael Shamoon`_, paperless now comes with a much more streamlined UI for
filtering documents. filtering documents.
* `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers. * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers.
* Added buttons to the management pages to quickly show all documents with one specific tag, correspondent, or title. * Added buttons to the management pages to quickly show all documents with one specific tag, correspondent, or title.
* Paperless now stores your saved views on the server and associates them with your user account. * Paperless now stores your saved views on the server and associates them with your user account.
This means that you can access your views on multiple devices and have separate views for different users. This means that you can access your views on multiple devices and have separate views for different users.
You will have to recreate your views. You will have to recreate your views.
@ -153,7 +237,7 @@ paperless-ng 0.9.7
This option enables you to be logged in into multiple instances by specifying different cookie names for each instance. This option enables you to be logged in into multiple instances by specifying different cookie names for each instance.
* Fixes * Fixes
* Sometimes paperless would assign dates in the future to newly consumed documents. * Sometimes paperless would assign dates in the future to newly consumed documents.
* The filename format fields ``{created_month}`` and ``{created_day}`` now use a leading zero for single digit values. * The filename format fields ``{created_month}`` and ``{created_day}`` now use a leading zero for single digit values.
* The filename format field ``{tags}`` can no longer be used without arguments. * The filename format field ``{tags}`` can no longer be used without arguments.

@ -53,6 +53,12 @@ PAPERLESS_DBPASS=<password>
Defaults to "paperless". Defaults to "paperless".
PAPERLESS_DBSSLMODE=<mode>
SSL mode to use when connecting to PostgreSQL.
See `the official documentation about sslmode <https://www.postgresql.org/docs/current/libpq-ssl.html>`_.
Default is ``prefer``.
Paths and folders Paths and folders
################# #################
@ -162,6 +168,12 @@ PAPERLESS_COOKIE_PREFIX=<str>
Defaults to ``""``, which does not alter the cookie names. Defaults to ``""``, which does not alter the cookie names.
PAPERLESS_ENABLE_HTTP_REMOTE_USER=<bool>
Allows authentication via HTTP_REMOTE_USER which is used by some SSO
applications.
Defaults to `false` which disables this feature.
.. _configuration-ocr: .. _configuration-ocr:
OCR settings OCR settings
@ -210,20 +222,20 @@ PAPERLESS_OCR_MODE=<mode>
into images and puts the OCRed text on top. This works for all documents, into images and puts the OCRed text on top. This works for all documents,
however, the resulting document may be significantly larger and text however, the resulting document may be significantly larger and text
won't appear as sharp when zoomed in. won't appear as sharp when zoomed in.
The default is ``skip``, which only performs OCR when necessary and always The default is ``skip``, which only performs OCR when necessary and always
creates archived documents. creates archived documents.
PAPERLESS_OCR_OUTPUT_TYPE=<type> PAPERLESS_OCR_OUTPUT_TYPE=<type>
Specify the the type of PDF documents that paperless should produce. Specify the the type of PDF documents that paperless should produce.
* ``pdf``: Modify the PDF document as little as possible. * ``pdf``: Modify the PDF document as little as possible.
* ``pdfa``: Convert PDF documents into PDF/A-2b documents, which is a * ``pdfa``: Convert PDF documents into PDF/A-2b documents, which is a
subset of the entire PDF specification and meant for storing subset of the entire PDF specification and meant for storing
documents long term. documents long term.
* ``pdfa-1``, ``pdfa-2``, ``pdfa-3`` to specify the exact version of * ``pdfa-1``, ``pdfa-2``, ``pdfa-3`` to specify the exact version of
PDF/A you wish to use. PDF/A you wish to use.
If not specified, ``pdfa`` is used. Remember that paperless also keeps If not specified, ``pdfa`` is used. Remember that paperless also keeps
the original input file as well as the archived version. the original input file as well as the archived version.
@ -255,7 +267,7 @@ PAPERLESS_OCR_IMAGE_DPI=<num>
present in an image. present in an image.
PAPERLESS_OCR_USER_ARG=<json> PAPERLESS_OCR_USER_ARGS=<json>
OCRmyPDF offers many more options. Use this parameter to specify any OCRmyPDF offers many more options. Use this parameter to specify any
additional arguments you wish to pass to OCRmyPDF. Since Paperless uses additional arguments you wish to pass to OCRmyPDF. Since Paperless uses
the API of OCRmyPDF, you have to specify these in a format that can be the API of OCRmyPDF, you have to specify these in a format that can be
@ -275,22 +287,19 @@ PAPERLESS_OCR_USER_ARG=<json>
.. code:: json .. code:: json
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"} {"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
.. _configuration-tika: .. _configuration-tika:
Tika settings Tika settings
############# #############
Paperless can make use of `Tika <https://tika.apache.org/>`_ and Paperless can make use of `Tika <https://tika.apache.org/>`_ and
`Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and `Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and
converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
wish to use this, you must provide a Tika server and a Gotenberg server, wish to use this, you must provide a Tika server and a Gotenberg server,
configure their endpoints, and enable the feature. configure their endpoints, and enable the feature.
If you run paperless on docker, you can add those services to the docker-compose
file (see the examples provided).
PAPERLESS_TIKA_ENABLED=<bool> PAPERLESS_TIKA_ENABLED=<bool>
Enable (or disable) the Tika parser. Enable (or disable) the Tika parser.
@ -306,7 +315,41 @@ PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>
Defaults to "http://localhost:3000". Defaults to "http://localhost:3000".
If you run paperless on docker, you can add those services to the docker-compose
file (see the provided ``docker-compose.tika.yml`` file for reference). The changes
requires are as follows:
.. code:: yaml
services:
# ...
webserver:
# ...
environment:
# ...
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
# ...
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
Add the configuration variables to the environment of the webserver (alternatively
put the configuration in the ``docker-compose.env`` file) and add the additional
services below the webserver service. Watch out for indentation.
Software tweaks Software tweaks
############### ###############
@ -333,8 +376,26 @@ PAPERLESS_THREADS_PER_WORKER=<num>
use a higher thread per worker count. use a higher thread per worker count.
The default is a balance between the two, according to your CPU core count, The default is a balance between the two, according to your CPU core count,
with a slight favor towards threads per worker, and using as much cores as with a slight favor towards threads per worker, and leaving at least one core
possible. free for other tasks:
+----------------+---------+---------+
| CPU core count | Workers | Threads |
+----------------+---------+---------+
| 1 | 1 | 1 |
+----------------+---------+---------+
| 2 | 1 | 1 |
+----------------+---------+---------+
| 4 | 1 | 3 |
+----------------+---------+---------+
| 6 | 2 | 2 |
+----------------+---------+---------+
| 8 | 2 | 3 |
+----------------+---------+---------+
| 12 | 3 | 3 |
+----------------+---------+---------+
| 16 | 3 | 5 |
+----------------+---------+---------+
If you only specify PAPERLESS_TASK_WORKERS, paperless will adjust If you only specify PAPERLESS_TASK_WORKERS, paperless will adjust
PAPERLESS_THREADS_PER_WORKER automatically. PAPERLESS_THREADS_PER_WORKER automatically.
@ -380,6 +441,9 @@ PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=<bool>
E.g. <CONSUMPTION_DIR>/foo/bar/file.pdf will add the tags "foo" and "bar" to E.g. <CONSUMPTION_DIR>/foo/bar/file.pdf will add the tags "foo" and "bar" to
the consumed file. Paperless will create any tags that don't exist yet. the consumed file. Paperless will create any tags that don't exist yet.
This is useful for sorting documents with certain tags such as ``car`` or
``todo`` prior to consumption. These folders won't be deleted.
PAPERLESS_CONSUMER_RECURSIVE must be enabled for this to work. PAPERLESS_CONSUMER_RECURSIVE must be enabled for this to work.
Defaults to false. Defaults to false.
@ -441,6 +505,19 @@ PAPERLESS_THUMBNAIL_FONT_NAME=<filename>
Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``. Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``.
PAPERLESS_IGNORE_DATES=<string>
Paperless parses a documents creation date from filename and file content.
You may specify a comma separated list of dates that should be ignored during
this process. This is useful for special dates (like date of birth) that appear
in documents regularly but are very unlikely to be the documents creation date.
You may specify dates in a multitude of formats supported by dateparser (see
https://dateparser.readthedocs.io/en/latest/#popular-formats) but as the dates
need to be comma separated, the options are limited.
Example: "2020-12-02,22.04.1999"
Defaults to an empty string to not ignore any dates.
Binaries Binaries
######## ########

@ -25,8 +25,9 @@ This section describes the steps you need to take to start development on paperl
* Python 3.6. * Python 3.6.
* All dependencies listed in the :ref:`Bare metal route <setup-bare_metal>` * All dependencies listed in the :ref:`Bare metal route <setup-bare_metal>`
* redis. You can either install redis or use the included scritps/start-redis.sh * redis. You can either install redis or use the included scritps/start-services.sh
to use docker to fire up a redis instance. to use docker to fire up a redis instance (and some other services such as tika,
gotenberg and a postgresql server).
Back end development Back end development
==================== ====================
@ -38,7 +39,7 @@ Install the python dependencies by performing ``pipenv install --dev`` in the sr
This will also create a virtual environment, which you can enter with ``pipenv shell`` or This will also create a virtual environment, which you can enter with ``pipenv shell`` or
execute one-shot commands in with ``pipenv run``. execute one-shot commands in with ``pipenv run``.
In ``src/paperless.conf``, enable debug mode. Copy ``paperless.conf.example`` to ``paperless.conf`` and enable debug mode.
Configure the IDE to use the src/ folder as the base source folder. Configure the following Configure the IDE to use the src/ folder as the base source folder. Configure the following
launch configurations in your IDE: launch configurations in your IDE:
@ -102,18 +103,11 @@ In order to build the front end and serve it as part of django, execute
.. code:: shell-session .. code:: shell-session
$ ng build --prod --output-path ../src/documents/static/frontend/ $ ng build --prod
This will build the front end and put it in a location from which the Django server will serve This will build the front end and put it in a location from which the Django server will serve
it as static content. This way, you can verify that authentication is working. it as static content. This way, you can verify that authentication is working.
Making a release
================
Execute the ``make-release.sh <ver>`` script.
This will test and assemble everything and also build and tag a docker image.
Extending Paperless Extending Paperless
=================== ===================

@ -52,6 +52,8 @@ out of that folder to use them elsewhere. Here are a couple notes about that.
* PDF documents, PNG images, JPEG images, TIFF images and GIF images are processed with OCR and converted into PDF documents. * PDF documents, PNG images, JPEG images, TIFF images and GIF images are processed with OCR and converted into PDF documents.
* Plain text documents are supported as well and are added verbatim * Plain text documents are supported as well and are added verbatim
to paperless. to paperless.
* With the optional Tika integration enabled (see :ref:`Configuration <configuration-tika>`), Paperless also supports various
Office documents (.docx, .doc, odt, .ppt, .pptx, .odp, .xls, .xlsx, .ods).
Paperless determines the type of a file by inspecting its content. The Paperless determines the type of a file by inspecting its content. The
file extensions do not matter. file extensions do not matter.
@ -73,10 +75,8 @@ in your browser and paperless has to do much less work to serve the data.
**Q:** *How do I install paperless-ng on Raspberry Pi?* **Q:** *How do I install paperless-ng on Raspberry Pi?*
**A:** There is no docker image for ARM available. If you know how to build **A:** Docker images are available for arm and arm64 hardware, so just follow
that automatically, I'm all ears. For now, you have to grab the latest release the docker-compose instructions, or go the bare metal route.
archive from the project page and build the image yourself. The release comes
with the front end already compiled, so you don't have to do this on the Pi.
**Q:** *How do I run this on unRaid?* **Q:** *How do I run this on unRaid?*

0
docs/requirements.txt Normal file

@ -10,6 +10,9 @@ scanner you use, but sometimes finding a scanner that will write to an FTP,
NFS, or SMB server can be difficult. This page is here to help you find one NFS, or SMB server can be difficult. This page is here to help you find one
that works right for you based on recommendations from other Paperless users. that works right for you based on recommendations from other Paperless users.
Physical scanners
=================
+---------+----------------+-----+-----+-----+----------------+ +---------+----------------+-----+-----+-----+----------------+
| Brand | Model | Supports | Recommended By | | Brand | Model | Supports | Recommended By |
+---------+----------------+-----+-----+-----+----------------+ +---------+----------------+-----+-----+-----+----------------+
@ -45,3 +48,25 @@ that works right for you based on recommendations from other Paperless users.
.. _REOLDEV: https://github.com/REOLDEV .. _REOLDEV: https://github.com/REOLDEV
.. _Skylinar: https://github.com/Skylinar .. _Skylinar: https://github.com/Skylinar
.. _jonaswinkler: https://github.com/jonaswinkler .. _jonaswinkler: https://github.com/jonaswinkler
Mobile phone software
=====================
You can use your phone to "scan" documents. The regular camera app will work, but may have too low contrast for OCR to work well. Apps specifically for scanning are recommended.
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
| Name | OS | Supports | Recommended By |
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
| | | FTP | NFS | SMB | Email | WebDav | |
+===================+================+=====+=====+=====+=======+========+================+
| `Office Lens`_ | Android | ? | ? | ? | ? | ? | `jonaswinkler`_|
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
| `Genius Scan`_ | Android | yes | no | yes | yes | yes | `hannahswain`_ |
+-------------------+----------------+-----+-----+-----+-------+--------+----------------+
On Android, you can use these applications in combination with one of the :ref:`Paperless-ng compatible apps <usage-mobile_upload>` to "Share" the documents produced by these scanner apps with paperless.
.. _Office Lens: https://play.google.com/store/apps/details?id=com.microsoft.office.officelens
.. _Genius Scan: https://play.google.com/store/apps/details?id=com.thegrizzlylabs.geniusscan.free
.. _hannahswain: https://github.com/hannahswain

@ -3,35 +3,6 @@
Setup Setup
***** *****
Download
########
Go to the project page on GitHub and download the
`latest release <https://github.com/jonaswinkler/paperless-ng/releases>`_.
There are multiple options available.
* Download the dockerfiles archive if you want to pull paperless from
Docker Hub.
* Download the dist archive and extract it if you want to build the docker image
yourself or want to install paperless without docker.
.. hint::
In contrast to paperless, the recommended way to get and update paperless-ng
is not to pull the entire git repository. Paperless-ng includes artifacts
that need to be compiled, and that's already done for you in the release.
.. admonition:: Want to try out paperless-ng before migrating?
The release contains a file ``.env`` which sets the docker-compose project
name to "paperless", which is the same as before and instructs docker-compose
to reuse and upgrade your paperless volumes.
Just rename the project name in that file to anything else and docker-compose
will create fresh volumes for you!
Overview of Paperless-ng Overview of Paperless-ng
######################## ########################
@ -110,22 +81,41 @@ Installation
You can go multiple routes with setting up and running Paperless: You can go multiple routes with setting up and running Paperless:
* The `docker route`_ * :ref:`Pull the image from Docker Hub <setup-docker_hub>`
* The `bare metal route`_ * :ref:`Build the Docker image yourself <setup-docker_build>`
* :ref:`Install Paperless directly on your system manually (bare metal) <setup-bare_metal>`
* :ref:`Use ansible to install Paperless on your system automatically (bare metal) <setup-ansible>`
The `docker route`_ is quick & easy. This is the recommended route. This configures all the stuff The Docker routes are quick & easy. These are the recommended routes. This configures all the stuff
from above automatically so that it just works and uses sensible defaults for all configuration options. from above automatically so that it just works and uses sensible defaults for all configuration options.
The `bare metal route`_ is more complicated to setup but makes it easier The bare metal route is more complicated to setup but makes it easier
should you want to contribute some code back. You need to configure and should you want to contribute some code back. You need to configure and
run the above mentioned components yourself. run the above mentioned components yourself.
.. _setup-docker_route: The ansible route cobines benefits from both options:
the setup process is fully automated, reproducible and idempotent,
it includes the same sensible defaults,
and it simultaneously provides the flexibility of a bare metal installation.
Docker Route .. _setup-docker_hub:
============
1. Install `Docker`_ and `docker-compose`_. [#compose]_ Install Paperless from Docker Hub
=================================
1. Go to the `/docker/compose directory on the project page <https://github.com/jonaswinkler/paperless-ng/tree/master/docker/compose>`_
and download one of the ``docker-compose.*.yml`` files, depending on which database backend you
want to use. Rename this file to `docker-compose.yml`.
If you want to enable optional support for Office documents, download a file with ``-tika`` in its name.
Download the ``docker-compose.env`` file and the ``.env`` file as well and store them
in the same directory.
.. hint::
For new installations, it is recommended to use PostgreSQL as the database
backend.
2. Install `Docker`_ and `docker-compose`_.
.. caution:: .. caution::
@ -142,15 +132,7 @@ Docker Route
.. _Docker installation guide: https://docs.docker.com/engine/installation/ .. _Docker installation guide: https://docs.docker.com/engine/installation/
.. _docker-compose installation guide: https://docs.docker.com/compose/install/ .. _docker-compose installation guide: https://docs.docker.com/compose/install/
2. Copy either ``docker-compose.sqlite.yml`` or ``docker-compose.postgres.yml`` to 3. Modify ``docker-compose.yml`` to your preferences. You may want to change the path
``docker-compose.yml``, depending on which database backend you want to use.
.. hint::
For new installations, it is recommended to use PostgreSQL as the database
backend.
2. Modify ``docker-compose.yml`` to your preferences. You may want to change the path
to the consumption directory in this file. Find the line that specifies where to the consumption directory in this file. Find the line that specifies where
to mount the consumption directory: to mount the consumption directory:
@ -167,7 +149,7 @@ Docker Route
Don't change the part after the colon or paperless wont find your documents. Don't change the part after the colon or paperless wont find your documents.
3. Modify ``docker-compose.env``, following the comments in the file. The 4. Modify ``docker-compose.env``, following the comments in the file. The
most important change is to set ``USERMAP_UID`` and ``USERMAP_GID`` most important change is to set ``USERMAP_UID`` and ``USERMAP_GID``
to the uid and gid of your user on the host system. This ensures that to the uid and gid of your user on the host system. This ensures that
both the docker container and you on the host machine have write access both the docker container and you on the host machine have write access
@ -177,9 +159,9 @@ Docker Route
.. note:: .. note::
You can use any settings from the file ``paperless.conf`` in this file. You can use any settings from the file ``paperless.conf.example`` in this file.
Have a look at :ref:`configuration` to see whats available. Have a look at :ref:`configuration` to see whats available.
.. caution:: .. caution::
Certain file systems such as NFS network shares don't support file system Certain file systems such as NFS network shares don't support file system
@ -188,11 +170,10 @@ Docker Route
with the default configuration. You will need to use ``PAPERLESS_CONSUMER_POLLING``, with the default configuration. You will need to use ``PAPERLESS_CONSUMER_POLLING``,
which will disable inotify. See :ref:`here <configuration-polling>`. which will disable inotify. See :ref:`here <configuration-polling>`.
4. Run ``docker-compose up -d``. This will create and start the necessary 5. Run ``docker-compose up -d``. This will create and start the necessary
containers. This will also build the image of paperless if you grabbed the containers.
source archive.
5. To be able to login, you will need a super user. To create it, execute the 6. To be able to login, you will need a super user. To create it, execute the
following command: following command:
.. code-block:: shell-session .. code-block:: shell-session
@ -202,7 +183,7 @@ Docker Route
This will prompt you to set a username, an optional e-mail address and This will prompt you to set a username, an optional e-mail address and
finally a password. finally a password.
6. The default ``docker-compose.yml`` exports the webserver on your local port 7. The default ``docker-compose.yml`` exports the webserver on your local port
8000. If you haven't adapted this, you should now be able to visit your 8000. If you haven't adapted this, you should now be able to visit your
Paperless instance at ``http://127.0.0.1:8000``. You can login with the Paperless instance at ``http://127.0.0.1:8000``. You can login with the
user and password you just created. user and password you just created.
@ -210,11 +191,49 @@ Docker Route
.. _Docker: https://www.docker.com/ .. _Docker: https://www.docker.com/
.. _docker-compose: https://docs.docker.com/compose/install/ .. _docker-compose: https://docs.docker.com/compose/install/
.. [#compose] You of course don't have to use docker-compose, but it .. _setup-docker_build:
simplifies deployment immensely. If you know your way around Docker, feel
free to tinker around without using compose!
.. _`setup-bare_metal`: Build the docker image yourself
===============================
1. Clone the entire repository of paperless:
.. code:: shell-session
git clone https://github.com/jonaswinkler/paperless-ng
The master branch always reflects the latest stable version.
2. Copy one of the ``docker/compose/docker-compose.*.yml`` to ``docker-compose.yml`` in the root folder,
depending on which database backend you want to use. Copy
``docker-compose.env`` into the project root as well.
3. In the ``docker-compose.yml`` file, find the line that instructs docker-compose to pull the paperless image from Docker Hub:
.. code:: yaml
webserver:
image: jonaswinkler/paperless-ng:latest
and replace it with a line that instructs docker-compose to build the image from the current working directory instead:
.. code:: yaml
webserver:
build: .
4. Run the ``compile-frontend.sh`` script. This requires ``node`` and ``npm >= v15``.
5. Follow steps 2 to 7 of :ref:`setup-docker_hub`. When asked to run
``docker-compose up -d`` to start the containers, do
.. code:: shell-session
$ docker-compose build
before that to build the image.
.. _setup-bare_metal:
Bare Metal Route Bare Metal Route
================ ================
@ -234,8 +253,9 @@ writing. Windows is not and will never be supported.
* ``optipng`` for optimizing thumbnails * ``optipng`` for optimizing thumbnails
* ``gnupg`` for handling encrypted documents * ``gnupg`` for handling encrypted documents
* ``libpoppler-cpp-dev`` for PDF to text conversion * ``libpoppler-cpp-dev`` for PDF to text conversion
* ``libmagic-dev`` for mime type detection
* ``libpq-dev`` for PostgreSQL * ``libpq-dev`` for PostgreSQL
* ``libmagic-dev`` for mime type detection
* ``mime-support`` for mime type detection
These dependencies are required for OCRmyPDF, which is used for text recognition. These dependencies are required for OCRmyPDF, which is used for text recognition.
@ -250,6 +270,11 @@ writing. Windows is not and will never be supported.
* ``tesseract-ocr`` >= 4.0.0 for OCR * ``tesseract-ocr`` >= 4.0.0 for OCR
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc) * ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
On Raspberry Pi, these libraries are required as well:
* ``libatlas-base-dev``
* ``libxslt1-dev``
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel`` You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
for installing some of the python dependencies. for installing some of the python dependencies.
@ -258,8 +283,9 @@ writing. Windows is not and will never be supported.
3. Optional. Install ``postgresql`` and configure a database, user and password for paperless. If you do not wish 3. Optional. Install ``postgresql`` and configure a database, user and password for paperless. If you do not wish
to use PostgreSQL, SQLite is avialable as well. to use PostgreSQL, SQLite is avialable as well.
4. Get the release archive. If you pull the git repo as it is, you also have to compile the front end by yourself. 4. Get the release archive from `<https://github.com/jonaswinkler/paperless-ng/releases>`_.
Extract the frontend to a place from where you wish to execute it, such as ``/opt/paperless``. If you clone the git repo as it is, you also have to compile the front end by yourself.
Extract the archive to a place from where you wish to execute it, such as ``/opt/paperless``.
5. Configure paperless. See :ref:`configuration` for details. Edit the included ``paperless.conf`` and adjust the 5. Configure paperless. See :ref:`configuration` for details. Edit the included ``paperless.conf`` and adjust the
settings to your needs. Required settings for getting paperless running are: settings to your needs. Required settings for getting paperless running are:
@ -272,7 +298,7 @@ writing. Windows is not and will never be supported.
paperless stores its data. If you like, you can point both to the same directory. paperless stores its data. If you like, you can point both to the same directory.
* ``PAPERLESS_SECRET_KEY`` should be a random sequence of characters. It's used for authentication. Failure * ``PAPERLESS_SECRET_KEY`` should be a random sequence of characters. It's used for authentication. Failure
to do so allows third parties to forge authentication credentials. to do so allows third parties to forge authentication credentials.
Many more adjustments can be made to paperless, especially the OCR part. The following options are recommended Many more adjustments can be made to paperless, especially the OCR part. The following options are recommended
for everyone: for everyone:
@ -281,7 +307,7 @@ writing. Windows is not and will never be supported.
6. Setup permissions. Create a system users under which you wish to run paperless. Ensure that these directories exist 6. Setup permissions. Create a system users under which you wish to run paperless. Ensure that these directories exist
and that the user has write permissions to the following directories and that the user has write permissions to the following directories
* ``/opt/paperless/media`` * ``/opt/paperless/media``
* ``/opt/paperless/data`` * ``/opt/paperless/data``
* ``/opt/paperless/consume`` * ``/opt/paperless/consume``
@ -295,14 +321,8 @@ writing. Windows is not and will never be supported.
.. code:: bash .. code:: bash
# This collects static files from paperless and django.
python3 manage.py collectstatic --clear --no-input
# This creates the database schema. # This creates the database schema.
python3 manage.py migrate python3 manage.py migrate
# This creates the translation files for paperless.
python3 manage.py compilemessages
# This creates your first paperless user # This creates your first paperless user
python3 manage.py createsuperuser python3 manage.py createsuperuser
@ -313,7 +333,7 @@ writing. Windows is not and will never be supported.
# This collects static files from paperless and django. # This collects static files from paperless and django.
python3 manage.py runserver python3 manage.py runserver
and pointing your browser to http://localhost:8000/. and pointing your browser to http://localhost:8000/.
.. warning:: .. warning::
@ -366,13 +386,133 @@ writing. Windows is not and will never be supported.
.. code:: .. code::
<policy domain="coder" rights="none" pattern="PDF" /> <policy domain="coder" rights="none" pattern="PDF" />
to to
.. code:: .. code::
<policy domain="coder" rights="read|write" pattern="PDF" /> <policy domain="coder" rights="read|write" pattern="PDF" />
13. Optional: Install the `jbig2enc <https://ocrmypdf.readthedocs.io/en/latest/jbig2.html>`_
encoder. This will reduce the size of generated PDF documents. You'll most likely need
to compile this by yourself, because this software has been patented until around 2017 and
binary packages are not available for most distributions.
.. _setup-ansible:
Install Paperless using ansible
===============================
.. note::
This role currently only supports Debian 10 Buster and Ubuntu 20.04 Focal or later as target hosts.
1. Install ansible 2.7+ on the management node.
This may be the target host paperless-ng is being installed on or any remote host which can access the target host.
For further details, check the ansible `inventory <https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html>`_ documentation.
On Debian and Ubuntu, the official repositories should provide a suitable version:
.. code:: bash
apt install ansible
ansible --version
Alternatively, you can install the most recent ansible release using PyPI:
.. code:: bash
python3 -m pip install ansible
ansible --version
Make sure your taget hosts are accessible:
.. code:: sh
ansible -m ping YourAnsibleTargetHostGoesHere
2. Clone the repository of paperless-ng:
.. code:: sh
git clone https://github.com/jonaswinkler/paperless-ng
Checkout the latest release tag:
.. code:: sh
cd paperless-ng
git checkout ng-0.9.14
3. Create an ansible ``playbook.yml`` in the paperless-ng root directory:
.. code:: yaml
- hosts: YourAnsibleTargetHostGoesHere
become: yes
vars_files:
- ansible/vars.yml
roles:
- ansible
Optional: If you also want to use PostgreSQL on the target system, install and add (for example) the `geerlingguy.postgresql <https://github.com/geerlingguy/ansible-role-postgresql>`_ role:
.. code:: sh
ansible-galaxy install geerlingguy.postgresql
.. code:: yaml
- hosts: YourAnsibleTargetHostGoesHere
become: yes
vars_files:
- ansible/vars.yml
roles:
- geerlingguy.postgresql
- ansible
Optional: If you also want to use a reverse proxy on the target system, install and add (for example) the `geerlingguy.nginx <https://github.com/geerlingguy/ansible-role-nginx>`_ role:
.. code:: sh
ansible-galaxy install geerlingguy.nginx
.. code:: yaml
- hosts: YourAnsibleTargetHostGoesHere
become: yes
vars_files:
- ansible/vars.yml
roles:
- geerlingguy.postgresql
- ansible
- geerlingguy.nginx
4. Create ``ansible/vars.yml`` to configure your ansible deployment:
.. code:: yaml
paperless_secret_key: PleaseGenerateAStrongKeyForThis
paperlessng_superuser_name: YourUserName
paperlessng_superuser_email: name@domain.tld
paperlessng_superuser_password: YourDesiredPasswordUsedForFirstLogin
paperlessng_ocr_languages:
- eng
- deu
For all of the available options, please check ``ansible/README.md`` and :ref:`configuration`.
Optional configurations for the above-mentioned PostgreSQL and nginx roles would also go here.
5. Run the ansible playbook from the management node:
.. code:: sh
ansible-playbook playbook.yml
When this step completes successfully, paperless-ng will be available on the target host at ``http://127.0.0.1:8000`` (or the address you configured).
Migration to paperless-ng Migration to paperless-ng
######################### #########################
@ -406,32 +546,27 @@ Migration to paperless-ng is then performed in a few simple steps:
paperless. paperless.
3. Download the latest release of paperless-ng. You can either go with the 3. Download the latest release of paperless-ng. You can either go with the
docker-compose files or use the archive to build the image yourself. docker-compose files from `here <https://github.com/jonaswinkler/paperless-ng/tree/master/docker/compose>`_
or clone the repository to build the image yourself (see :ref:`above <setup-docker_build>`).
You can either replace your current paperless folder or put paperless-ng You can either replace your current paperless folder or put paperless-ng
in a different location. in a different location.
.. caution:: .. caution::
The release include a ``.env`` file. This will set the Paperless includes a ``.env`` file. This will set the
project name for docker compose to ``paperless`` so that paperless-ng will project name for docker compose to ``paperless`` so that paperless-ng will
automatically reuse your existing paperless volumes. When you start it, it automatically reuse your existing paperless volumes. When you start it, it
will migrate your existing data. After that, your old paperless installation will migrate your existing data. After that, your old paperless installation
will be incompatible with the migrated volumes. will be incompatible with the migrated volumes.
4. Copy the ``docker-compose.sqlite.yml`` file to ``docker-compose.yml``. 4. Download the ``docker-compose.sqlite.yml`` file to ``docker-compose.yml``.
If you want to switch to PostgreSQL, do that after you migrated your existing If you want to switch to PostgreSQL, do that after you migrated your existing
SQLite database. SQLite database.
5. Adjust ``docker-compose.yml`` and 5. Adjust ``docker-compose.yml`` and ``docker-compose.env`` to your needs.
``docker-compose.env`` to your needs. See :ref:`setup-docker_hub` for details on which edits are advised.
See `docker route`_ for details on which edits are advised.
6. Since ``docker-compose`` would just use the the old paperless image, we need to 6. :ref:`Update paperless. <administration-updating>`
manually build a new image:
.. code:: shell-session
$ docker-compose build
7. In order to find your existing documents with the new search feature, you need 7. In order to find your existing documents with the new search feature, you need
to invoke a one-time operation that will create the search index: to invoke a one-time operation that will create the search index:
@ -439,7 +574,7 @@ Migration to paperless-ng is then performed in a few simple steps:
.. code:: shell-session .. code:: shell-session
$ docker-compose run --rm webserver document_index reindex $ docker-compose run --rm webserver document_index reindex
This will migrate your database and create the search index. After that, This will migrate your database and create the search index. After that,
paperless will take care of maintaining the index by itself. paperless will take care of maintaining the index by itself.
@ -452,7 +587,7 @@ Migration to paperless-ng is then performed in a few simple steps:
This will run paperless in the background and automatically start it on system boot. This will run paperless in the background and automatically start it on system boot.
9. Paperless installed a permanent redirect to ``admin/`` in your browser. This 9. Paperless installed a permanent redirect to ``admin/`` in your browser. This
redirect is still in place and prevents access to the new UI. Clear redirect is still in place and prevents access to the new UI. Clear your
browsing cache in order to fix this. browsing cache in order to fix this.
10. Optionally, follow the instructions below to migrate your existing data to PostgreSQL. 10. Optionally, follow the instructions below to migrate your existing data to PostgreSQL.
@ -500,9 +635,9 @@ management commands as below.
$ cd /path/to/paperless $ cd /path/to/paperless
$ docker-compose run --rm webserver /bin/bash $ docker-compose run --rm webserver /bin/bash
This will launch the container and initialize the PostgreSQL database. This will launch the container and initialize the PostgreSQL database.
b) Without docker, open a shell in your virtual environment, switch to b) Without docker, open a shell in your virtual environment, switch to
the ``src`` directory and create the database schema: the ``src`` directory and create the database schema:
@ -512,7 +647,7 @@ management commands as below.
$ pipenv shell $ pipenv shell
$ cd src $ cd src
$ python3 manage.py migrate $ python3 manage.py migrate
This will not copy any data yet. This will not copy any data yet.
4. Dump your data from SQLite: 4. Dump your data from SQLite:
@ -520,7 +655,7 @@ management commands as below.
.. code:: shell-session .. code:: shell-session
$ python3 manage.py dumpdata --database=sqlite --exclude=contenttypes --exclude=auth.Permission > data.json $ python3 manage.py dumpdata --database=sqlite --exclude=contenttypes --exclude=auth.Permission > data.json
5. Load your data into PostgreSQL: 5. Load your data into PostgreSQL:
.. code:: shell-session .. code:: shell-session
@ -571,7 +706,7 @@ as well.
Considerations for less powerful devices Considerations for less powerful devices
######################################## ########################################
Paperless runs on Raspberry Pi. However, some things are rather slow on the Pi and Paperless runs on Raspberry Pi. However, some things are rather slow on the Pi and
configuring some options in paperless can help improve performance immensely: configuring some options in paperless can help improve performance immensely:
* Stick with SQLite to save some resources. * Stick with SQLite to save some resources.
@ -593,17 +728,15 @@ configuring some options in paperless can help improve performance immensely:
For details, refer to :ref:`configuration`. For details, refer to :ref:`configuration`.
.. note:: .. note::
Updating the :ref:`automatic matching algorithm <advanced-automatic_matching>` Updating the :ref:`automatic matching algorithm <advanced-automatic_matching>`
takes quite a bit of time. However, the update mechanism checks if your takes quite a bit of time. However, the update mechanism checks if your
data has changed before doing the heavy lifting. If you experience the data has changed before doing the heavy lifting. If you experience the
algorithm taking too much cpu time, consider changing the schedule in the algorithm taking too much cpu time, consider changing the schedule in the
admin interface to daily. You can also manually invoke the task admin interface to daily. You can also manually invoke the task
by changing the date and time of the next run to today/now. by changing the date and time of the next run to today/now.
The actual matching of the algorithm is fast and works on Raspberry Pi as The actual matching of the algorithm is fast and works on Raspberry Pi as
well as on any other device. well as on any other device.
.. _redis: https://redis.io/ .. _redis: https://redis.io/

@ -75,6 +75,6 @@ You might encounter errors such as:
This happens when paperless does not have permission to delete files inside the consumption directory. This happens when paperless does not have permission to delete files inside the consumption directory.
Ensure that ``USERMAP_UID`` and ``USERMAP_GID`` are set to the user id and group id you use on the host operating system, if these are Ensure that ``USERMAP_UID`` and ``USERMAP_GID`` are set to the user id and group id you use on the host operating system, if these are
different from ``1000``. See :ref:`setup-docker_route`. different from ``1000``. See :ref:`setup-docker_hub`.
Also ensure that you are able to read and write to the consumption directory on the host. Also ensure that you are able to read and write to the consumption directory on the host.

@ -110,6 +110,8 @@ The dashboard has a file drop field to upload documents to paperless. Simply dra
onto this field or select a file with the file dialog. Multiple files are supported. onto this field or select a file with the file dialog. Multiple files are supported.
.. _usage-mobile_upload:
Mobile upload Mobile upload
============= =============
@ -118,7 +120,7 @@ to share any documents with paperless. This can be combined with any of the mobi
scanning apps out there, such as Office Lens. scanning apps out there, such as Office Lens.
Furthermore, there is the `Paperless App <https://github.com/bauerj/paperless_app>`_ as well, Furthermore, there is the `Paperless App <https://github.com/bauerj/paperless_app>`_ as well,
which no only has document upload, but also document editing and browsing. which not only has document upload, but also document browsing and download features.
.. _usage-email: .. _usage-email:

@ -13,6 +13,7 @@
#PAPERLESS_DBNAME=paperless #PAPERLESS_DBNAME=paperless
#PAPERLESS_DBUSER=paperless #PAPERLESS_DBUSER=paperless
#PAPERLESS_DBPASS=paperless #PAPERLESS_DBPASS=paperless
#PAPERLESS_DBSSLMODE=prefer
# Paths and folders # Paths and folders
@ -26,11 +27,12 @@
#PAPERLESS_SECRET_KEY=change-me #PAPERLESS_SECRET_KEY=change-me
#PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com #PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com
#PAPERLESS_CORS_ALLOWED_HOSTS=localhost:8080,example.com,localhost:8000 #PAPERLESS_CORS_ALLOWED_HOSTS=http://example.com,http://localhost:8000
#PAPERLESS_FORCE_SCRIPT_NAME= #PAPERLESS_FORCE_SCRIPT_NAME=
#PAPERLESS_STATIC_URL=/static/ #PAPERLESS_STATIC_URL=/static/
#PAPERLESS_AUTO_LOGIN_USERNAME= #PAPERLESS_AUTO_LOGIN_USERNAME=
#PAPERLESS_COOKIE_PREFIX= #PAPERLESS_COOKIE_PREFIX=
#PAPERLESS_ENABLE_HTTP_REMOTE_USER=false
# OCR settings # OCR settings
@ -50,11 +52,14 @@
#PAPERLESS_TIME_ZONE=UTC #PAPERLESS_TIME_ZONE=UTC
#PAPERLESS_CONSUMER_POLLING=10 #PAPERLESS_CONSUMER_POLLING=10
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
#PAPERLESS_CONSUMER_RECURSIVE=false
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
#PAPERLESS_OPTIMIZE_THUMBNAILS=true #PAPERLESS_OPTIMIZE_THUMBNAILS=true
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
#PAPERLESS_FILENAME_DATE_ORDER=YMD #PAPERLESS_FILENAME_DATE_ORDER=YMD
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
#PAPERLESS_THUMBNAIL_FONT_NAME= #PAPERLESS_THUMBNAIL_FONT_NAME=
#PAPERLESS_IGNORE_DATES=
# Tika settings # Tika settings

74
requirements.txt Normal file

@ -0,0 +1,74 @@
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
# pipenv lock --requirements
#
-i https://pypi.python.org/simple
--extra-index-url https://www.piwheels.org/simple
arrow==0.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
asgiref==3.3.1; python_version >= '3.5'
blessed==1.17.12
certifi==2020.12.5
cffi==1.14.4
chardet==4.0.0; python_version >= '3.1'
coloredlogs==15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
cryptography==3.3.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
dateparser==0.7.6
django-cors-headers==3.6.0
django-extensions==3.1.0
django-filter==2.4.0
django-picklefield==3.0.1; python_version >= '3'
django-q==1.3.4
django==3.1.5
djangorestframework==3.12.2
filelock==3.0.12
fuzzywuzzy==0.18.0
gunicorn==20.0.4
humanfriendly==9.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
imap-tools==0.34.0
img2pdf==0.4.0
importlib-metadata==3.4.0; python_version < '3.8'
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
inotifyrecursive==0.3.5
joblib==1.0.0; python_version >= '3.6'
langdetect==1.0.8
lxml==4.6.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
numpy==1.19.5; python_version >= '3.6'
ocrmypdf==11.4.5
pathvalidate==2.3.2
pdfminer.six==20201018; python_version >= '3.4'
pdftotext==2.1.5
pikepdf==2.2.5
pillow==8.1.0
pluggy==0.13.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
psycopg2-binary==2.8.6
pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dateutil==2.8.1
python-dotenv==0.15.0
python-gnupg==0.4.6
python-levenshtein==0.12.0
python-magic==0.4.18
pytz==2020.5
redis==3.5.3
regex==2020.11.13
reportlab==3.5.59
requests==2.25.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
scikit-learn==0.24.0
scipy==1.5.4; python_version >= '3.6'
six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sortedcontainers==2.3.0
sqlparse==0.4.1; python_version >= '3.5'
threadpoolctl==2.1.0; python_version >= '3.5'
tika==1.24
tqdm==4.56.0
typing-extensions==3.7.4.3; python_version < '3.8'
tzlocal==2.1
urllib3==1.26.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
watchdog==1.0.2
wcwidth==0.2.5
whitenoise==5.2.0
whoosh==2.7.4
zipp==3.4.0; python_version >= '3.6'

@ -1,125 +0,0 @@
#!/bin/bash
# Release checklist
# - wait for travis build.
# adjust src/paperless/version.py
# changelog in the documentation
# adjust versions in docker/hub/*
# adjust version in src-ui/src/environments/prod
# If docker-compose was modified: all compose files are the same.
# Steps:
# run release script "dev", push
# if it works: new tag, merge into master
# on master: make release "lastest", push
# on master: make release "version-tag", push
# publish release files
set -e
VERSION=$1
if [ -z "$VERSION" ]
then
echo "Need a version string."
exit 1
fi
# source root directory of paperless
PAPERLESS_ROOT=$(git rev-parse --show-toplevel)
# output directory
PAPERLESS_DIST="$PAPERLESS_ROOT/dist"
PAPERLESS_DIST_APP="$PAPERLESS_DIST/paperless-ng"
PAPERLESS_DIST_DOCKERFILES="$PAPERLESS_DIST/paperless-ng-dockerfiles"
if [ -d "$PAPERLESS_DIST" ]
then
echo "Removing $PAPERLESS_DIST"
rm "$PAPERLESS_DIST" -r
fi
mkdir "$PAPERLESS_DIST"
mkdir "$PAPERLESS_DIST_APP"
mkdir "$PAPERLESS_DIST_APP/docker"
mkdir "$PAPERLESS_DIST_APP/scripts"
mkdir "$PAPERLESS_DIST_DOCKERFILES"
# setup dependencies.
cd "$PAPERLESS_ROOT"
pipenv clean
pipenv install --dev
pipenv lock --keep-outdated -r > "$PAPERLESS_DIST_APP/requirements.txt"
# test if the application works.
cd "$PAPERLESS_ROOT/src"
pipenv run pytest --cov
pipenv run pycodestyle
# make the documentation.
cd "$PAPERLESS_ROOT/docs"
make clean html
# copy stuff into place
# the application itself
cp "$PAPERLESS_ROOT/.env" \
"$PAPERLESS_ROOT/.dockerignore" \
"$PAPERLESS_ROOT/CONTRIBUTING.md" \
"$PAPERLESS_ROOT/LICENSE" \
"$PAPERLESS_ROOT/Pipfile" \
"$PAPERLESS_ROOT/Pipfile.lock" \
"$PAPERLESS_ROOT/README.md" "$PAPERLESS_DIST_APP"
cp "$PAPERLESS_ROOT/paperless.conf.example" "$PAPERLESS_DIST_APP/paperless.conf"
# copy python source, templates and static files.
cd "$PAPERLESS_ROOT"
find src -wholename '*/templates/*' -o -wholename '*/static/*' -o -name '*.py' | cpio -pdm "$PAPERLESS_DIST_APP"
# build the front end.
cd "$PAPERLESS_ROOT/src-ui"
ng build --prod --output-hashing none --sourceMap=false --output-path "$PAPERLESS_DIST_APP/src/documents/static/frontend"
# documentation
cp "$PAPERLESS_ROOT/docs/_build/html/" "$PAPERLESS_DIST_APP/docs" -r
# docker files for building the image yourself
cp "$PAPERLESS_ROOT/docker/local/"* "$PAPERLESS_DIST_APP"
cp "$PAPERLESS_ROOT/docker/docker-compose.env" "$PAPERLESS_DIST_APP"
# docker files for pulling from docker hub
cp "$PAPERLESS_ROOT/docker/hub/"* "$PAPERLESS_DIST_DOCKERFILES"
cp "$PAPERLESS_ROOT/.env" "$PAPERLESS_DIST_DOCKERFILES"
cp "$PAPERLESS_ROOT/docker/docker-compose.env" "$PAPERLESS_DIST_DOCKERFILES"
# auxiliary files required for the docker image
cp "$PAPERLESS_ROOT/docker/docker-entrypoint.sh" "$PAPERLESS_DIST_APP/docker/"
cp "$PAPERLESS_ROOT/docker/gunicorn.conf.py" "$PAPERLESS_DIST_APP/docker/"
cp "$PAPERLESS_ROOT/docker/imagemagick-policy.xml" "$PAPERLESS_DIST_APP/docker/"
cp "$PAPERLESS_ROOT/docker/supervisord.conf" "$PAPERLESS_DIST_APP/docker/"
# auxiliary files for bare metal installs
cp "$PAPERLESS_ROOT/scripts/paperless-webserver.service" "$PAPERLESS_DIST_APP/scripts/"
cp "$PAPERLESS_ROOT/scripts/paperless-consumer.service" "$PAPERLESS_DIST_APP/scripts/"
cp "$PAPERLESS_ROOT/scripts/paperless-scheduler.service" "$PAPERLESS_DIST_APP/scripts/"
# try to make the docker build.
cd "$PAPERLESS_DIST_APP"
docker build . -t "jonaswinkler/paperless-ng:$VERSION"
# works. package the app!
cd "$PAPERLESS_DIST"
tar -cJf "paperless-ng-$VERSION.tar.xz" paperless-ng/
tar -cJf "paperless-ng-$VERSION-dockerfiles.tar.xz" paperless-ng-dockerfiles/

@ -1,23 +0,0 @@
#!/bin/bash
set -e
VERSION=$1
if [ -z "$VERSION" ]
then
echo "Need a version string."
exit 1
fi
# source root directory of paperless
PAPERLESS_ROOT=$(git rev-parse --show-toplevel)
# output directory
PAPERLESS_DIST="$PAPERLESS_ROOT/dist"
PAPERLESS_DIST_APP="$PAPERLESS_DIST/paperless-ng"
cd "$PAPERLESS_DIST_APP"
docker push "jonaswinkler/paperless-ng:$VERSION"

@ -58,6 +58,7 @@
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
} }
], ],
"outputPath": "../src/documents/static/frontend/",
"optimization": true, "optimization": true,
"outputHashing": "none", "outputHashing": "none",
"sourceMap": false, "sourceMap": false,

File diff suppressed because it is too large Load Diff

@ -331,6 +331,12 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true
},
"uuid": { "uuid": {
"version": "8.3.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
@ -2178,6 +2184,14 @@
"pacote": "9.5.12", "pacote": "9.5.12",
"semver": "7.3.2", "semver": "7.3.2",
"semver-intersect": "1.4.0" "semver-intersect": "1.4.0"
},
"dependencies": {
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true
}
} }
}, },
"@types/glob": { "@types/glob": {

@ -13,7 +13,7 @@ import { DocumentTypeListComponent } from './components/manage/document-type-lis
import { LogsComponent } from './components/manage/logs/logs.component'; import { LogsComponent } from './components/manage/logs/logs.component';
import { SettingsComponent } from './components/manage/settings/settings.component'; import { SettingsComponent } from './components/manage/settings/settings.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common'; import { DatePipe, registerLocaleData } from '@angular/common';
import { NotFoundComponent } from './components/not-found/not-found.component'; import { NotFoundComponent } from './components/not-found/not-found.component';
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'; import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component';
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component';
@ -58,8 +58,18 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'; import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { NumberComponent } from './components/common/input/number/number.component'; import { NumberComponent } from './components/common/input/number/number.component';
import { SafePipe } from './pipes/safe.pipe';
import { CustomDatePipe } from './pipes/custom-date.pipe';
import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component'; import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component';
import localeFr from '@angular/common/locales/fr';
import localeNl from '@angular/common/locales/nl';
import localeDe from '@angular/common/locales/de';
registerLocaleData(localeFr)
registerLocaleData(localeNl)
registerLocaleData(localeDe)
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -108,6 +118,8 @@ import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/co
MetadataCollapseComponent, MetadataCollapseComponent,
SelectDialogComponent, SelectDialogComponent,
NumberComponent, NumberComponent,
SafePipe,
CustomDatePipe,
ConsumerStatusWidgetComponent ConsumerStatusWidgetComponent
], ],
imports: [ imports: [

@ -140,6 +140,13 @@
</svg>&nbsp;<ng-container i18n>Logs</ng-container> </svg>&nbsp;<ng-container i18n>Logs</ng-container>
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg>&nbsp;<ng-container i18n>Settings</ng-container>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="admin/"> <a class="nav-link" href="admin/">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">

@ -113,7 +113,7 @@
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);
padding-left: 1.8rem; padding-left: 1.8rem;
border-color: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.2);
transition: flex 0.3s ease; transition: all .3s ease, padding-left 0s ease, background-color 0s ease; // Safari requires all
max-width: 600px; max-width: 600px;
min-width: 300px; // 1/2 max min-width: 300px; // 1/2 max

@ -9,7 +9,7 @@
<p *ngIf="message">{{message}}</p> <p *ngIf="message">{{message}}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled">Cancel</button> <button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}} {{btnCaption}}
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span> <span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>

@ -1,4 +1,4 @@
<div class="btn-group" ngbDropdown role="group"> <div class="btn-group w-100" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'"> <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
{{title}} {{title}}
</button> </button>

@ -27,6 +27,8 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
networkActive = false networkActive = false
closeEnabled = false
error = null error = null
abstract getForm(): FormGroup abstract getForm(): FormGroup
@ -37,6 +39,11 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
if (this.object != null) { if (this.object != null) {
this.objectForm.patchValue(this.object) this.objectForm.patchValue(this.object)
} }
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
setTimeout(() => {
this.closeEnabled = true
});
} }
getCreateTitle() { getCreateTitle() {
@ -86,7 +93,6 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
serverResponse.subscribe(result => { serverResponse.subscribe(result => {
this.activeModal.close() this.activeModal.close()
this.success.emit(result) this.success.emit(result)
this.networkActive = false
}, error => { }, error => {
this.error = error.error this.error = error.error
this.networkActive = false this.networkActive = false

@ -1,11 +1,9 @@
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown"> <div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'"> <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
<div class="d-none d-md-inline">{{title}}</div> <svg class="toolbaricon" fill="currentColor">
<div class="d-inline-block d-md-none"> <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
<svg class="toolbaricon" fill="currentColor"> </svg>
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> <div class="d-none d-sm-inline">&nbsp;{{title}}</div>
</svg>
</div>
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0"> <ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
<div class="badge bg-secondary text-light rounded-pill badge-corner"> <div class="badge bg-secondary text-light rounded-pill badge-corner">
{{selectionModel.selectionSize()}} {{selectionModel.selectionSize()}}
@ -20,7 +18,7 @@
</div> </div>
</div> </div>
<div *ngIf="selectionModel.items" class="items"> <div *ngIf="selectionModel.items" class="items">
<ng-container *ngFor="let item of (editing ? selectionModel.itemsSorted : selectionModel.items) | filter: filterText"> <ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button> <app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
</ng-container> </ng-container>
</div> </div>

@ -19,8 +19,13 @@ export class FilterableDropdownSelectionModel {
items: MatchingModel[] = [] items: MatchingModel[] = []
get itemsSorted(): MatchingModel[] { get itemsSorted(): MatchingModel[] {
// TODO: this is getting called very often
return this.items.sort((a,b) => { return this.items.sort((a,b) => {
if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) { if (a.id == null && b.id != null) {
return -1
} else if (a.id != null && b.id == null) {
return 1
} else if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
return 1 return 1
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) { } else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
return -1 return -1

@ -1,10 +1,13 @@
import { Directive, Input, OnInit } from '@angular/core'; import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms'; import { ControlValueAccessor } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@Directive() @Directive()
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor { export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
@ViewChild("inputField")
inputField: ElementRef
constructor() { } constructor() { }
onChange = (newValue: T) => {}; onChange = (newValue: T) => {};
@ -24,6 +27,12 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
this.disabled = isDisabled; this.disabled = isDisabled;
} }
focus() {
if (this.inputField && this.inputField.nativeElement) {
this.inputField.nativeElement.focus()
}
}
@Input() @Input()
title: string title: string

@ -29,7 +29,7 @@ export class NumberComponent extends AbstractInputComponent<number> {
if (results.count > 0) { if (results.count > 0) {
this.value = results.results[0].archive_serial_number + 1 this.value = results.results[0].archive_serial_number + 1
} else { } else {
this.value + 1 this.value = 1
} }
this.onChange(this.value) this.onChange(this.value)
} }

@ -6,9 +6,11 @@
[style.color]="textColor" [style.color]="textColor"
[style.background]="backgroundColor" [style.background]="backgroundColor"
[clearable]="allowNull" [clearable]="allowNull"
[items]="items"
bindLabel="name"
bindValue="id"
(change)="onChange(value)" (change)="onChange(value)"
(blur)="onTouched()"> (blur)="onTouched()">
<ng-option *ngFor="let i of items" [value]="i.id">{{i.name}}</ng-option>
</ng-select> </ng-select>
<div *ngIf="showPlusButton()" class="input-group-append"> <div *ngIf="showPlusButton()" class="input-group-append">

@ -1,5 +1,5 @@
<div class="form-group paperless-input-select paperless-input-tags"> <div class="form-group paperless-input-select paperless-input-tags">
<label for="tags">Tags</label> <label for="tags" i18n>Tags</label>
<div class="input-group flex-nowrap"> <div class="input-group flex-nowrap">
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue" <ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"

@ -1,6 +1,6 @@
<div class="form-group"> <div class="form-group">
<label [for]="inputId">{{title}}</label> <label [for]="inputId">{{title}}</label>
<input type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{error}}

@ -1,9 +1,9 @@
<div class="row pt-3 pb-1 mb-3 border-bottom align-items-center" > <div class="row pt-3 pb-3 pb-md-1 mb-3 border-bottom align-items-center">
<div class="col-md text-truncate"> <div class="col-md text-truncate">
<p class="h2 text-truncate" style="line-height: 1.4">{{title}}</p> <p class="h2 text-truncate" style="line-height: 1.4">{{title}}</p>
<p *ngIf="subTitle" class="h5 text-truncate" style="line-height: 1.4">{{subTitle}}</p> <p *ngIf="subTitle" class="h5 text-truncate" style="line-height: 1.4">{{subTitle}}</p>
</div> </div>
<div class="btn-toolbar col-auto"> <div class="btn-toolbar col col-md-auto">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</div> </div>

File diff suppressed because one or more lines are too long

@ -12,7 +12,7 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}"> <tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}">
<td>{{doc.created | date}}</td> <td>{{doc.created | customDate}}</td>
<td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag> <td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag>
</tr> </tr>
</tbody> </tbody>

@ -1,7 +1,8 @@
table { table {
overflow-wrap: anywhere; overflow-wrap: anywhere;
table-layout: fixed;
} }
th:first-child { th:first-child {
min-width: 5rem; width: 25%;
} }

@ -50,7 +50,7 @@ export class SavedViewWidgetComponent implements OnInit {
} else { } else {
this.list.load(this.savedView) this.list.load(this.savedView)
this.router.navigate(["documents"]) this.router.navigate(["documents"])
} }
} }
} }

@ -1,5 +1,5 @@
<app-page-header [(title)]="title"> <app-page-header [(title)]="title">
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'"> <div class="input-group input-group-sm mr-5 d-none d-md-flex" *ngIf="getContentType() == 'application/pdf' && !useNativePdfViewer">
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text" i18n>Page</div> <div class="input-group-text" i18n>Page</div>
</div> </div>
@ -9,7 +9,7 @@
</div> </div>
</div> </div>
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()"> <button type="button" class="btn btn-sm btn-outline-danger mr-2 ml-auto" (click)="delete()">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Delete</span> </svg>&nbsp;<span class="d-none d-lg-inline" i18n>Delete</span>
@ -56,14 +56,14 @@
<a ngbNavLink i18n>Details</a> <a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<app-input-text i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text> <app-input-text #inputTitle i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number> <app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time> <app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
(createNew)="createCorrespondent()"></app-input-select> (createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" <app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
(createNew)="createDocumentType()"></app-input-select> (createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" i18n-title title="Tags"></app-input-tags> <app-input-tags formControlName="tags"></app-input-tags>
</ng-template> </ng-template>
</li> </li>
@ -85,11 +85,11 @@
<tbody> <tbody>
<tr> <tr>
<td i18n>Date modified</td> <td i18n>Date modified</td>
<td>{{document.modified | date:'medium'}}</td> <td>{{document.modified | customDate}}</td>
</tr> </tr>
<tr> <tr>
<td i18n>Date added</td> <td i18n>Date added</td>
<td>{{document.added | date:'medium'}}</td> <td>{{document.added | customDate}}</td>
</tr> </tr>
<tr> <tr>
<td i18n>Media filename</td> <td i18n>Media filename</td>
@ -134,8 +134,17 @@
</div> </div>
<div class="col-md-6 col-xl-8 mb-3"> <div class="col-md-6 col-xl-8 mb-3">
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'"> <ng-container *ngIf="getContentType() == 'application/pdf'">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer> <div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
</div> <pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
<ng-template #nativePdfViewer>
<object [data]="previewUrl | safe" type="application/pdf" class="preview-sticky" width="100%"></object>
</ng-template>
</ng-container>
<ng-container *ngIf="getContentType() == 'text/plain'">
<object [data]="previewUrl | safe" type="text/plain" class="preview-sticky" width="100%"></object>
</ng-container>
</div> </div>
</div> </div>

@ -1,6 +1,9 @@
.pdf-viewer-container { .preview-sticky {
height: calc(100vh - 160px); height: calc(100vh - 160px);
top: 70px; top: 70px;
position: sticky; position: sticky;
}
.pdf-viewer-container {
background-color: gray; background-color: gray;
} }

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@ -17,6 +17,8 @@ import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/c
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
import { PDFDocumentProxy } from 'ng2-pdf-viewer'; import { PDFDocumentProxy } from 'ng2-pdf-viewer';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { TextComponent } from '../common/input/text/text.component';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
@Component({ @Component({
selector: 'app-document-detail', selector: 'app-document-detail',
@ -25,6 +27,9 @@ import { ToastService } from 'src/app/services/toast.service';
}) })
export class DocumentDetailComponent implements OnInit { export class DocumentDetailComponent implements OnInit {
@ViewChild("inputTitle")
titleInput: TextComponent
expandOriginalMetadata = false expandOriginalMetadata = false
expandArchivedMetadata = false expandArchivedMetadata = false
@ -66,7 +71,12 @@ export class DocumentDetailComponent implements OnInit {
private openDocumentService: OpenDocumentsService, private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService, private documentListViewService: DocumentListViewService,
private documentTitlePipe: DocumentTitlePipe, private documentTitlePipe: DocumentTitlePipe,
private toastService: ToastService) { } private toastService: ToastService,
private settings: SettingsService) { }
get useNativePdfViewer(): boolean {
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
}
getContentType() { getContentType() {
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
@ -157,6 +167,7 @@ export class DocumentDetailComponent implements OnInit {
if (nextDocId) { if (nextDocId) {
this.openDocumentService.closeDocument(this.document) this.openDocumentService.closeDocument(this.document)
this.router.navigate(['documents', nextDocId]) this.router.navigate(['documents', nextDocId])
this.titleInput.focus()
} }
}, error => { }, error => {
this.networkActive = false this.networkActive = false

@ -6,8 +6,7 @@
</svg>&nbsp;<ng-container i18n>Cancel</ng-container> </svg>&nbsp;<ng-container i18n>Cancel</ng-container>
</button> </button>
</div> </div>
<div class="w-100 d-xl-none"></div> <div class="col-auto mb-2 mb-xl-0 ml-auto ml-md-0" role="group" aria-label="Select">
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
<label class="mr-2 mb-0" i18n>Select:</label> <label class="mr-2 mb-0" i18n>Select:</label>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()"> <button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
@ -56,9 +55,8 @@
</app-filterable-dropdown> </app-filterable-dropdown>
</div> </div>
</div> </div>
<div class="w-100 d-xl-none"></div> <div class="col-auto ml-auto mb-2 mb-xl-0 d-flex">
<div class="col mb-2 mb-xl-0 d-flex"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
<button type="button" class="btn btn-sm btn-outline-danger ml-0 ml-lg-auto" (click)="applyDelete()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container> </svg>&nbsp;<ng-container i18n>Delete</ng-container>

@ -1,11 +1,11 @@
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable"> <div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected"> <div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
<img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left" (click)="setSelected(selectable ? !selected : false)"> <img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left">
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected"> <div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)"> <input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label> <label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
</div> </div>
</div> </div>
@ -17,11 +17,11 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="card-title"> <h5 class="card-title">
<ng-container *ngIf="document.correspondent"> <ng-container *ngIf="document.correspondent">
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a> <a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: <ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container> </ng-container>
{{document.title | documentTitle}} {{document.title | documentTitle}}
<app-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag> <app-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></app-tag>
</h5> </h5>
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5> <h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
</div> </div>
@ -31,38 +31,40 @@
</p> </p>
<div class="d-flex align-items-center"> <div class="d-flex flex-column flex-md-row align-items-md-center">
<div class="btn-group"> <div class="btn-group">
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis"> <a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/> <path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>&nbsp;<ng-container i18n>More like this</ng-container> </svg>&nbsp;<span class="d-block d-md-inline" i18n>More like this</span>
</a> </a>
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary"> <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>&nbsp;<ng-container i18n>Edit</ng-container> </svg>&nbsp;<span class="d-block d-md-inline" i18n>Edit</span>
</a> </a>
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()"> <a class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/> <path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/> <path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
</svg>&nbsp;<ng-container i18n>View</ng-container> </svg>&nbsp;<span class="d-block d-md-inline" i18n>View</span>
</a> </a>
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>&nbsp;<ng-container i18n>Download</ng-container> </svg>&nbsp;<span class="d-block d-md-inline" i18n>Download</span>
</a> </a>
</div> </div>
<small *ngIf="searchScore" class="text-muted ml-auto" i18n>Score:</small> <div *ngIf="searchScore" class="d-flex align-items-center ml-md-auto mt-2 mt-md-0">
<small class="text-muted" i18n>Score:</small>
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar> <ngb-progressbar [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
</div>
<small class="text-muted" [class.ml-auto]="!searchScore" i18n>Created: {{document.created | date}}</small> <small class="text-muted" [class.ml-auto]="!searchScore" i18n>Created: {{document.created | customDate}}</small>
</div> </div>
</div> </div>

@ -12,10 +12,14 @@
mix-blend-mode: multiply; mix-blend-mode: multiply;
} }
.card-title {
word-break: break-word;
}
.search-score-bar { .search-score-bar {
width: 100px; width: 100px;
height: 5px; height: 5px;
margin-top: 2px; margin-top: 1px;
} }
.document-card-check { .document-card-check {

@ -15,16 +15,11 @@ export class DocumentCardLargeComponent implements OnInit {
@Input() @Input()
selected = false selected = false
setSelected(value: boolean) {
this.selected = value
this.selectedChange.emit(value)
}
@Output() @Output()
selectedChange = new EventEmitter<boolean>() toggleSelected = new EventEmitter()
get selectable() { get selectable() {
return this.selectedChange.observers.length > 0 return this.toggleSelected.observers.length > 0
} }
@Input() @Input()

@ -1,18 +1,18 @@
<div class="col p-2 h-100"> <div class="col p-2 h-100">
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected"> <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected"> <div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
<img class="card-img doc-img rounded-top" [src]="getThumbUrl()" (click)="setSelected(!selected)"> <img class="card-img doc-img rounded-top" [src]="getThumbUrl()">
<div class="border-right border-bottom bg-light p-1 rounded document-card-check"> <div class="border-right border-bottom bg-light p-1 rounded document-card-check">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)"> <input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label> <label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
</div> </div>
</div> </div>
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1"> <div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
<div *ngFor="let t of getTagsLimited$() | async"> <div *ngFor="let t of getTagsLimited$() | async">
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag> <app-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
</div> </div>
<div *ngIf="moreTags"> <div *ngIf="moreTags">
<span class="badge badge-secondary">+ {{moreTags}}</span> <span class="badge badge-secondary">+ {{moreTags}}</span>
@ -23,9 +23,9 @@
<div class="card-body p-2"> <div class="card-body p-2">
<p class="card-text"> <p class="card-text">
<ng-container *ngIf="document.correspondent"> <ng-container *ngIf="document.correspondent">
<a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>: <a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
</ng-container> </ng-container>
{{document.title | documentTitle}} {{document.title | documentTitle}} <span *ngIf="document.archive_serial_number">(#{{document.archive_serial_number}})</span>
</p> </p>
</div> </div>
<div class="card-footer"> <div class="card-footer">
@ -43,14 +43,14 @@
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/> <path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
</svg> </svg>
</a> </a>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title> <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" (click)="$event.stopPropagation()" i18n-title>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg> </svg>
</a> </a>
</div> </div>
<small class="text-muted pl-1">{{document.created | date}}</small> <small class="text-muted pl-1">{{document.created | customDate:'shortDate'}}</small>
</div> </div>
</div> </div>

@ -14,14 +14,9 @@ export class DocumentCardSmallComponent implements OnInit {
@Input() @Input()
selected = false selected = false
setSelected(value: boolean) {
this.selected = value
this.selectedChange.emit(value)
}
@Output() @Output()
selectedChange = new EventEmitter<boolean>() toggleSelected = new EventEmitter()
@Input() @Input()
document: PaperlessDocument document: PaperlessDocument

@ -1,11 +1,10 @@
<app-page-header [title]="getTitle()"> <app-page-header [title]="getTitle()">
<div ngbDropdown class="d-inline-block mr-2"> <div ngbDropdown class="mr-2 flex-fill d-flex">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary flex-fill" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" /> <use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
</svg>&nbsp;<ng-container i18n>Select</ng-container> </svg>&nbsp;<ng-container i18n>Select</ng-container>
</button> </button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button> <button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
@ -14,7 +13,7 @@
</div> </div>
</div> </div>
<div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode" <div class="btn-group btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="displayMode"
(ngModelChange)="saveDisplayMode()"> (ngModelChange)="saveDisplayMode()">
<label ngbButtonLabel class="btn-outline-primary btn-sm"> <label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" value="details"> <input ngbButton type="radio" class="btn btn-sm" value="details">
@ -36,49 +35,48 @@
</label> </label>
</div> </div>
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse"> <div ngbDropdown class="btn-group ml-2 flex-fill">
<div ngbDropdown class="btn-group"> <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button>
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort by</button> <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right">
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow"> <div class="w-100 d-flex btn-group-toggle pb-2 mb-1 border-bottom" ngbRadioGroup [(ngModel)]="list.sortReverse">
<label ngbButtonLabel class="btn-outline-primary btn-sm mx-2 flex-fill">
<input ngbButton type="radio" class="btn btn-sm" [value]="false">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
</svg>
</label>
<label ngbButtonLabel class="btn-outline-primary btn-sm mr-2 flex-fill">
<input ngbButton type="radio" class="btn btn-sm" [value]="true">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
</svg>
</label>
</div>
<div>
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field" <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
[class.active]="list.sortField == f.field">{{f.name}}</button> [class.active]="list.sortField == f.field">{{f.name}}
</button>
</div> </div>
</div> </div>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" [value]="false">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
</svg>
</label>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" [value]="true">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
</svg>
</label>
</div> </div>
<div class="btn-group ml-2"> <div class="btn-group ml-2 flex-fill" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" ngbDropdownToggle i18n>Views</button>
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
<ng-container *ngIf="!list.savedViewId">
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
</ng-container>
<div class="btn-group" ngbDropdown role="group"> <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle i18n>Views</button> <button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<ng-container *ngIf="!list.savedViewId">
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
</ng-container>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
</div>
</div> </div>
</div> </div>
</app-page-header> </app-page-header>
<div class="w-100 mb-2 mb-sm-4"> <div class="w-100 mb-2 mb-sm-4">
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor> <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [rulesModified]="filterRulesModified" (filterRulesChange)="rulesChanged()" (reset)="resetFilters()" #filterEditor></app-filter-editor>
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor> <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
</div> </div>
@ -92,7 +90,7 @@
</div> </div>
<div *ngIf="displayMode == 'largeCards'"> <div *ngIf="displayMode == 'largeCards'">
<app-document-card-large [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"> <app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
</app-document-card-large> </app-document-card-large>
</div> </div>
@ -104,55 +102,43 @@
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n> i18n>ASN</th>
ASN
</th>
<th class="d-none d-md-table-cell" <th class="d-none d-md-table-cell"
sortable="correspondent__name" sortable="correspondent__name"
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n> i18n>Correspondent</th>
Correspondent
</th>
<th <th
sortable="title" sortable="title"
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n> i18n>Title</th>
Title
</th>
<th class="d-none d-xl-table-cell" <th class="d-none d-xl-table-cell"
sortable="document_type__name" sortable="document_type__name"
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n> i18n>Document type</th>
Document type
</th>
<th <th
sortable="created" sortable="created"
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n> i18n>Created</th>
Created
</th>
<th class="d-none d-xl-table-cell" <th class="d-none d-xl-table-cell"
sortable="added" sortable="added"
[currentSortField]="list.sortField" [currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n> i18n>Added</th>
Added
</th>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> <tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" (click)="toggleSelected(d, $event)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td> <td>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (change)="list.setSelected(d, $event.target.checked)"> <input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event)">
<label class="custom-control-label" for="docCheck{{d.id}}"></label> <label class="custom-control-label" for="docCheck{{d.id}}"></label>
</div> </div>
</td> </td>
@ -161,28 +147,28 @@
</td> </td>
<td class="d-none d-md-table-cell"> <td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent"> <ng-container *ngIf="d.correspondent">
<a [routerLink]="" (click)="clickCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> <a [routerLink]="" (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
</ng-container> </ng-container>
</td> </td>
<td> <td>
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> <a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id)"></app-tag> <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
</td> </td>
<td class="d-none d-xl-table-cell"> <td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type"> <ng-container *ngIf="d.document_type">
<a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> <a [routerLink]="" (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
</ng-container> </ng-container>
</td> </td>
<td> <td>
{{d.created | date}} {{d.created | customDate}}
</td> </td>
<td class="d-none d-xl-table-cell"> <td class="d-none d-xl-table-cell">
{{d.added | date}} {{d.added | customDate}}
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> <div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small> <app-document-card-small [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
</div> </div>

@ -1,5 +1,9 @@
@import "/src/theme"; @import "/src/theme";
tr {
user-select: none;
}
.table-row-selected { .table-row-selected {
background-color: $primaryFaded; background-color: $primaryFaded;
} }
@ -25,3 +29,8 @@ $paperless-card-breakpoints: (
} }
} }
} }
.dropdown-menu-right {
right: 0 !important;
left: auto !important;
}

@ -33,6 +33,8 @@ export class DocumentListComponent implements OnInit {
displayMode = 'smallCards' // largeCards, smallCards, details displayMode = 'smallCards' // largeCards, smallCards, details
filterRulesModified: boolean = false
get isFiltered() { get isFiltered() {
return this.list.filterRules?.length > 0 return this.list.filterRules?.length > 0
} }
@ -69,13 +71,14 @@ export class DocumentListComponent implements OnInit {
this.router.navigate(["404"]) this.router.navigate(["404"])
return return
} }
this.list.savedView = view this.list.savedView = view
this.list.reload() this.list.reload()
this.rulesChanged()
}) })
} else { } else {
this.list.savedView = null this.list.savedView = null
this.list.reload() this.list.reload()
this.rulesChanged()
} }
}) })
} }
@ -83,6 +86,7 @@ export class DocumentListComponent implements OnInit {
loadViewConfig(view: PaperlessSavedView) { loadViewConfig(view: PaperlessSavedView) {
this.list.load(view) this.list.load(view)
this.list.reload() this.list.reload()
this.rulesChanged()
} }
saveViewConfig() { saveViewConfig() {
@ -105,6 +109,7 @@ export class DocumentListComponent implements OnInit {
sort_reverse: this.list.sortReverse, sort_reverse: this.list.sortReverse,
sort_field: this.list.sortField sort_field: this.list.sortField
} }
this.savedViewService.create(savedView).subscribe(() => { this.savedViewService.create(savedView).subscribe(() => {
modal.close() modal.close()
this.toastService.showInfo($localize`View "${savedView.name}" created successfully.`) this.toastService.showInfo($localize`View "${savedView.name}" created successfully.`)
@ -115,6 +120,51 @@ export class DocumentListComponent implements OnInit {
}) })
} }
resetFilters(): void {
this.filterRulesModified = false
if (this.list.savedViewId) {
this.savedViewService.getCached(this.list.savedViewId).subscribe(viewUntouched => {
this.list.filterRules = viewUntouched.filter_rules
this.list.reload()
})
} else {
this.list.filterRules = []
this.list.reload()
}
}
rulesChanged() {
let modified = false
if (this.list.savedView == null) {
modified = this.list.filterRules.length > 0 // documents list is modified if it has any filters
} else {
// compare savedView current filters vs original
this.savedViewService.getCached(this.list.savedViewId).subscribe(view => {
let filterRulesInitial = view.filter_rules
if (this.list.filterRules.length !== filterRulesInitial.length) modified = true
else {
modified = this.list.filterRules.some(rule => {
return (filterRulesInitial.find(fri => fri.rule_type == rule.rule_type && fri.value == rule.value) == undefined)
})
if (!modified) {
// only check other direction if we havent already determined is modified
modified = filterRulesInitial.some(rule => {
this.list.filterRules.find(fr => fr.rule_type == rule.rule_type && fr.value == rule.value) == undefined
})
}
}
})
}
this.filterRulesModified = modified
}
toggleSelected(document: PaperlessDocument, event: MouseEvent): void {
if (!event.shiftKey) this.list.toggleSelected(document)
else this.list.selectRangeTo(document)
}
clickTag(tagID: number) { clickTag(tagID: number) {
this.list.selectNone() this.list.selectNone()
setTimeout(() => { setTimeout(() => {

@ -1,33 +1,36 @@
<div class="row"> <div class="row">
<div class="col mb-2 mb-xl-0"> <div class="col mb-2 mb-xl-0">
<div class="form-inline d-flex"> <div class="form-inline d-flex align-items-center">
<label class="text-muted mr-2" i18n>Filter by:</label> <label class="text-muted mr-2 mb-0" i18n>Filter by:</label>
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title" i18n-placeholder> <input class="form-control form-control-sm flex-fill w-auto" type="text" [(ngModel)]="titleFilter" placeholder="Title" i18n-placeholder>
</div> </div>
</div> </div>
<div class="w-100 d-xl-none"></div> <div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0"> <div class="col col-xl-auto mb-2 mb-xl-0">
<div class="d-flex"> <div class="d-flex">
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title <app-filterable-dropdown class="mr-2 flex-fill" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags" [items]="tags"
[(selectionModel)]="tagSelectionModel" [(selectionModel)]="tagSelectionModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
[multiple]="true" [multiple]="true"
(open)="onTagsDropdownOpen()"
[allowSelectNone]="true"></app-filterable-dropdown> [allowSelectNone]="true"></app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title <app-filterable-dropdown class="mr-2 flex-fill" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents" [items]="correspondents"
[(selectionModel)]="correspondentSelectionModel" [(selectionModel)]="correspondentSelectionModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(open)="onCorrespondentDropdownOpen()"
[allowSelectNone]="true"></app-filterable-dropdown> [allowSelectNone]="true"></app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title <app-filterable-dropdown class="mr-2 flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes" [items]="documentTypes"
[(selectionModel)]="documentTypeSelectionModel" [(selectionModel)]="documentTypeSelectionModel"
(open)="onDocumentTypeDropdownOpen()"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
[allowSelectNone]="true"></app-filterable-dropdown> [allowSelectNone]="true"></app-filterable-dropdown>
<app-date-dropdown class="mr-2 mr-md-3" <app-date-dropdown class="mr-2"
title="Created" i18n-title title="Created" i18n-title
(datesSet)="updateRules()" (datesSet)="updateRules()"
[(dateBefore)]="dateCreatedBefore" [(dateBefore)]="dateCreatedBefore"
@ -41,11 +44,11 @@
</div> </div>
<div class="w-100 d-xl-none"></div> <div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0"> <div class="col col-xl-auto mb-2 mb-xl-0">
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!hasFilters()" (click)="clearSelected()"> <button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!rulesModified" (click)="resetSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" 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>&nbsp;<ng-container i18n>Clear all filters</ng-container> </svg>&nbsp;<ng-container i18n>Reset filters</ng-container>
</button> </button>
</div> </div>
</div> </div>

@ -40,7 +40,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
case FILTER_HAS_TAG: case FILTER_HAS_TAG:
return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}` return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
case FILTER_HAS_ANY_TAG: case FILTER_HAS_ANY_TAG:
if (rule.value == "false") { if (rule.value == "false") {
return $localize`Without any tag` return $localize`Without any tag`
@ -127,7 +127,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
} else { } else {
this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => { this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => {
filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id?.toString()}) filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id?.toString()})
}) })
} }
this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => { this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()}) filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()})
@ -153,16 +153,16 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
@Output() @Output()
filterRulesChange = new EventEmitter<FilterRule[]>() filterRulesChange = new EventEmitter<FilterRule[]>()
@Output()
reset = new EventEmitter()
@Input()
rulesModified: boolean = false
updateRules() { updateRules() {
this.filterRulesChange.next(this.filterRules) this.filterRulesChange.next(this.filterRules)
} }
hasFilters() {
return this._titleFilter ||
this.dateAddedAfter || this.dateAddedBefore || this.dateCreatedAfter || this.dateCreatedBefore ||
this.tagSelectionModel.selectionSize() || this.correspondentSelectionModel.selectionSize() || this.documentTypeSelectionModel.selectionSize()
}
get titleFilter() { get titleFilter() {
return this._titleFilter return this._titleFilter
} }
@ -194,16 +194,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
this.titleFilterDebounce.complete() this.titleFilterDebounce.complete()
} }
clearSelected() { resetSelected() {
this._titleFilter = "" this.reset.next()
this.tagSelectionModel.clear(false)
this.documentTypeSelectionModel.clear(false)
this.correspondentSelectionModel.clear(false)
this.dateAddedBefore = null
this.dateAddedAfter = null
this.dateCreatedBefore = null
this.dateCreatedAfter = null
this.updateRules()
} }
toggleTag(tagId: number) { toggleTag(tagId: number) {
@ -218,4 +210,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
this.documentTypeSelectionModel.toggle(documentTypeId) this.documentTypeSelectionModel.toggle(documentTypeId)
} }
onTagsDropdownOpen() {
this.tagSelectionModel.apply()
}
onCorrespondentDropdownOpen() {
this.correspondentSelectionModel.apply()
}
onDocumentTypeDropdownOpen() {
this.documentTypeSelectionModel.apply()
}
} }

@ -1,7 +1,7 @@
<form [formGroup]="saveViewConfigForm" (ngSubmit)="save()"> <form [formGroup]="saveViewConfigForm" (ngSubmit)="save()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Save current view</h4> <h4 class="modal-title" id="modal-basic-title" i18n>Save current view</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>

@ -20,6 +20,8 @@ export class SaveViewConfigDialogComponent implements OnInit {
@Input() @Input()
buttonsEnabled = true buttonsEnabled = true
closeEnabled = false
_defaultName = "" _defaultName = ""
get defaultName() { get defaultName() {
@ -31,7 +33,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
this._defaultName = value this._defaultName = value
this.saveViewConfigForm.patchValue({name: value}) this.saveViewConfigForm.patchValue({name: value})
} }
saveViewConfigForm = new FormGroup({ saveViewConfigForm = new FormGroup({
name: new FormControl(''), name: new FormControl(''),
showInSideBar: new FormControl(false), showInSideBar: new FormControl(false),
@ -39,6 +41,10 @@ export class SaveViewConfigDialogComponent implements OnInit {
}) })
ngOnInit(): void { ngOnInit(): void {
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
setTimeout(() => {
this.closeEnabled = true
});
} }
save() { save() {

@ -1,12 +1,11 @@
<form [formGroup]="objectForm" (ngSubmit)="save()"> <form [formGroup]="objectForm" (ngSubmit)="save()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text> <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>

@ -2,11 +2,18 @@
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button> <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
</app-page-header> </app-page-header>
<div class="row m-0 justify-content-end"> <div class="row">
<ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination> <div class="col-md mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center">
<label class="text-muted mr-2 mb-0" i18n>Filter by:</label>
<input class="form-control form-control-sm flex-fill w-auto" type="text" [(ngModel)]="nameFilter" placeholder="Name" i18n-placeholder>
</div>
</div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
</div> </div>
<table class="table table-striped border shadow"> <table class="table table-striped border shadow-sm">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> <th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
@ -21,7 +28,7 @@
<td scope="row">{{ correspondent.name }}</td> <td scope="row">{{ correspondent.name }}</td>
<td scope="row">{{ getMatching(correspondent) }}</td> <td scope="row">{{ getMatching(correspondent) }}</td>
<td scope="row">{{ correspondent.document_count }}</td> <td scope="row">{{ correspondent.document_count }}</td>
<td scope="row">{{ correspondent.last_correspondence | date }}</td> <td scope="row">{{ correspondent.last_correspondence | customDate }}</td>
<td scope="row"> <td scope="row">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)"> <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)">

@ -1,7 +1,7 @@
<form [formGroup]="objectForm" (ngSubmit)="save()"> <form [formGroup]="objectForm" (ngSubmit)="save()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>

@ -2,12 +2,18 @@
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button> <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
</app-page-header> </app-page-header>
<div class="row m-0 justify-content-end"> <div class="row">
<ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" <div class="col-md mb-2 mb-xl-0">
aria-label="Default pagination"></ngb-pagination> <div class="form-inline d-flex align-items-center">
<label class="text-muted mr-2 mb-0" i18n>Filter by:</label>
<input class="form-control form-control-sm flex-fill w-auto" type="text" [(ngModel)]="nameFilter" placeholder="Name" i18n-placeholder>
</div>
</div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
</div> </div>
<table class="table table-striped border shadow"> <table class="table table-striped border shadow-sm">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th> <th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>

@ -1,17 +1,19 @@
import { Directive, OnInit, QueryList, ViewChildren } from '@angular/core'; import { Directive, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'; import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
import { ObjectWithId } from 'src/app/data/object-with-id'; import { ObjectWithId } from 'src/app/data/object-with-id';
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive'; import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component';
@Directive() @Directive()
export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit { export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit, OnDestroy {
constructor( constructor(
private service: AbstractPaperlessService<T>, private service: AbstractNameFilterService<T>,
private modalService: NgbModal, private modalService: NgbModal,
private editDialogComponent: any, private editDialogComponent: any,
private toastService: ToastService) { private toastService: ToastService) {
@ -28,6 +30,10 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
public sortField: string public sortField: string
public sortReverse: boolean public sortReverse: boolean
private nameFilterDebounce: Subject<string>
private subscription: Subscription
private _nameFilter: string
getMatching(o: MatchingModel) { getMatching(o: MatchingModel) {
if (o.matching_algorithm == MATCH_AUTO) { if (o.matching_algorithm == MATCH_AUTO) {
return $localize`Automatic` return $localize`Automatic`
@ -44,12 +50,27 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
this.reloadData() this.reloadData()
} }
ngOnInit(): void { ngOnInit(): void {
this.reloadData() this.reloadData()
this.nameFilterDebounce = new Subject<string>()
this.subscription = this.nameFilterDebounce.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(title => {
this._nameFilter = title
this.reloadData()
})
}
ngOnDestroy() {
this.subscription.unsubscribe()
} }
reloadData() { reloadData() {
this.service.list(this.page, null, this.sortField, this.sortReverse).subscribe(c => { this.service.listFiltered(this.page, null, this.sortField, this.sortReverse, this._nameFilter).subscribe(c => {
this.data = c.results this.data = c.results
this.collectionSize = c.count this.collectionSize = c.count
}); });
@ -95,4 +116,12 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
} }
) )
} }
get nameFilter() {
return this._nameFilter
}
set nameFilter(nameFilter: string) {
this.nameFilterDebounce.next(nameFilter)
}
} }

@ -5,8 +5,8 @@
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" /> <use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg>&nbsp;<ng-container i18n>Filter</ng-container> </svg>&nbsp;<ng-container i18n>Filter</ng-container>
</button> </button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1"> <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button *ngFor="let f of getLevels()" ngbDropdownItem (click)="setLevel(f.id)" <button *ngFor="let f of getLevels()" ngbDropdownItem (click)="setLevel(f.id)"
@ -16,12 +16,12 @@
</app-page-header> </app-page-header>
<div class="bg-dark p-3 mb-3" infiniteScroll (scrolled)="onScroll()"> <div class="bg-dark p-3 mb-3 text-light text-monospace" infiniteScroll (scrolled)="onScroll()">
<p <p
class="text-light text-monospace m-0 p-0 log-entry-{{log.level}}" class="m-0 p-0 log-entry-{{log.level}}"
*ngFor="let log of logs"> *ngFor="let log of logs">
{{log.created | date:'short'}} {{log.created | customDate:'short'}}
{{getLevelText(log.level)}} {{getLevelText(log.level)}}
{{log.message}} {{log.message}}
</p> </p>
</div> </div>

@ -12,6 +12,56 @@
<h4 i18n>Appearance</h4> <h4 i18n>Appearance</h4>
<div class="form-row form-group">
<div class="col-md-3 col-form-label">
<span i18n>Display language</span>
</div>
<div class="col">
<select class="form-control" formControlName="displayLanguage">
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale != 'en-US'"> - {{lang.englishName}}</span></option>
</select>
<small class="form-text text-muted" i18n>You need to reload the page after applying a new language.</small>
</div>
</div>
<div class="form-row form-group">
<div class="col-md-3 col-form-label">
<span i18n>Date display</span>
</div>
<div class="col">
<select class="form-control" formControlName="dateLocale">
<option *ngFor="let lang of dateLocaleOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code"> - {{today | date:'shortDate':null:lang.code}}</span></option>
</select>
</div>
</div>
<div class="form-row form-group">
<div class="col-md-3 col-form-label">
<span i18n>Date format</span>
</div>
<div class="col">
<div class="custom-control custom-radio">
<input type="radio" id="dateFormatShort" name="dateFormat" class="custom-control-input" formControlName="dateFormat" value="shortDate">
<label class="custom-control-label" for="dateFormatShort" i18n>Short: {{today | customDate:'shortDate':null:computedDateLocale}}</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="dateFormatMedium" name="dateFormat" class="custom-control-input" formControlName="dateFormat" value="mediumDate">
<label class="custom-control-label" for="dateFormatMedium" i18n>Medium: {{today | customDate:'mediumDate':null:computedDateLocale}}</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="dateFormatLong" name="dateFormat" class="custom-control-input" formControlName="dateFormat" value="longDate">
<label class="custom-control-label" for="dateFormatLong" i18n>Long: {{today | customDate:'longDate':null:computedDateLocale}}</label>
</div>
</div>
</div>
<div class="form-row form-group"> <div class="form-row form-group">
<div class="col-md-3 col-form-label"> <div class="col-md-3 col-form-label">
<span i18n>Items per page</span> <span i18n>Items per page</span>
@ -28,16 +78,24 @@
</div> </div>
</div> </div>
<div class="form-row form-group">
<div class="col-md-3 col-form-label">
<span i18n>Document editor</span>
</div>
<div class="col">
<app-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></app-input-check>
</div>
</div>
<div class="form-row form-group"> <div class="form-row form-group">
<div class="col-md-3 col-form-label"> <div class="col-md-3 col-form-label">
<span i18n>Dark mode</span> <span i18n>Dark mode</span>
</div> </div>
<div class="col"> <div class="col">
<app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem" (change)="toggleDarkModeSetting()"></app-input-check> <app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem"></app-input-check>
<div class="custom-control custom-switch" *ngIf="!settingsForm.value.darkModeUseSystem"> <app-input-check [hidden]="settingsForm.value.darkModeUseSystem" i18n-title title="Enable dark mode" formControlName="darkModeEnabled"></app-input-check>
<input type="checkbox" class="custom-control-input" id="darkModeEnabled" formControlName="darkModeEnabled" [checked]="settingsForm.value.darkModeEnabled">
<label class="custom-control-label" for="darkModeEnabled">Enabled</label>
</div>
</div> </div>
</div> </div>
@ -92,5 +150,5 @@
<div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div> <div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary" i18n>Save</button>
</form> </form>

@ -1,9 +1,9 @@
import { Component, OnInit, Renderer2 } from '@angular/core'; import { Component, Inject, LOCALE_ID, OnInit, Renderer2 } 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';
import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; import { LanguageOption, SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@Component({ @Component({
@ -21,16 +21,25 @@ export class SettingsComponent implements OnInit {
'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)), 'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)), 'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)),
'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)), 'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)),
'savedViews': this.savedViewGroup 'useNativePdfViewer': new FormControl(this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)),
'savedViews': this.savedViewGroup,
'displayLanguage': new FormControl(this.settings.getLanguage()),
'dateLocale': new FormControl(this.settings.get(SETTINGS_KEYS.DATE_LOCALE)),
'dateFormat': new FormControl(this.settings.get(SETTINGS_KEYS.DATE_FORMAT)),
}) })
savedViews: PaperlessSavedView[] savedViews: PaperlessSavedView[]
get computedDateLocale(): string {
return this.settingsForm.value.dateLocale || this.settingsForm.value.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
) { } ) { }
ngOnInit() { ngOnInit() {
@ -55,25 +64,33 @@ export class SettingsComponent implements OnInit {
}) })
} }
toggleDarkModeSetting() {
if (this.settingsForm.value.darkModeUseSystem) {
(this.settingsForm.controls.darkModeEnabled as FormControl).disable()
} else {
(this.settingsForm.controls.darkModeEnabled as FormControl).enable()
}
}
private saveLocalSettings() { private saveLocalSettings() {
this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose) this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose)
this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs) this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs)
this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem) this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem)
this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString()) this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString())
this.settings.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, this.settingsForm.value.useNativePdfViewer)
this.settings.set(SETTINGS_KEYS.DATE_LOCALE, this.settingsForm.value.dateLocale)
this.settings.set(SETTINGS_KEYS.DATE_FORMAT, this.settingsForm.value.dateFormat)
this.settings.setLanguage(this.settingsForm.value.displayLanguage)
this.documentListViewService.updatePageSize() this.documentListViewService.updatePageSize()
this.settings.updateDarkModeSettings() this.settings.updateDarkModeSettings()
this.toastService.showInfo($localize`Settings saved successfully.`) this.toastService.showInfo($localize`Settings saved successfully.`)
} }
get displayLanguageOptions(): LanguageOption[] {
return [{code: "", name: $localize`Use system language`}].concat(this.settings.getLanguageOptions())
}
get dateLocaleOptions(): LanguageOption[] {
return [{code: "", name: $localize`Use date format of display language`}].concat(this.settings.getLanguageOptions())
}
get today() {
return new Date()
}
saveSettings() { saveSettings() {
let x = [] let x = []
for (let id in this.savedViewGroup.value) { for (let id in this.savedViewGroup.value) {

@ -1,12 +1,12 @@
<form [formGroup]="objectForm" (ngSubmit)="save()"> <form [formGroup]="objectForm" (ngSubmit)="save()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()"> <button type="button" [disabled]="!closeEnabled" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<app-input-text title="Name" formControlName="name" [error]="error?.name"></app-input-text> <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<div class="form-group paperless-input-select"> <div class="form-group paperless-input-select">

@ -2,9 +2,15 @@
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button> <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
</app-page-header> </app-page-header>
<div class="row m-0 justify-content-end"> <div class="row">
<ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" <div class="col-md mb-2 mb-xl-0">
aria-label="Default pagination"></ngb-pagination> <div class="form-inline d-flex align-items-center">
<label class="text-muted mr-2 mb-0" i18n>Filter by:</label>
<input class="form-control form-control-sm flex-fill w-auto" type="text" [(ngModel)]="nameFilter" placeholder="Name" i18n-placeholder>
</div>
</div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
</div> </div>
<table class="table table-striped border shadow-sm"> <table class="table table-striped border shadow-sm">

@ -16,11 +16,13 @@
<div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()"> <div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()">
<p i18n>{resultCount, plural, =0 {No results} =1 {One result} other {{{resultCount}} results}}</p> <p i18n>{resultCount, plural, =0 {No results} =1 {One result} other {{{resultCount}} results}}</p>
<app-document-card-large *ngFor="let result of results" <ng-container *ngFor="let result of results">
[document]="result.document" <app-document-card-large *ngIf="result.document"
[details]="result.highlights" [document]="result.document"
[searchScore]="result.score / maxScore" [details]="result.highlights"
[moreLikeThis]="true"> [searchScore]="result.score / maxScore"
[moreLikeThis]="true">
</app-document-card-large>
</ng-container>
</app-document-card-large>
</div> </div>

@ -22,37 +22,36 @@ export const FILTER_ASN_ISNULL = 18
export const FILTER_RULE_TYPES: FilterRuleType[] = [ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""}, {id: FILTER_TITLE, filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
{id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false, default: ""}, {id: FILTER_CONTENT, filtervar: "content__icontains", datatype: "string", multi: false, default: ""},
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, {id: FILTER_ASN, filtervar: "archive_serial_number", datatype: "number", multi: false},
{id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", isnull_filtervar: "correspondent__isnull", datatype: "correspondent", multi: false}, {id: FILTER_CORRESPONDENT, filtervar: "correspondent__id", isnull_filtervar: "correspondent__isnull", datatype: "correspondent", multi: false},
{id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", isnull_filtervar: "document_type__isnull", datatype: "document_type", multi: false}, {id: FILTER_DOCUMENT_TYPE, filtervar: "document_type__id", isnull_filtervar: "document_type__isnull", datatype: "document_type", multi: false},
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true}, {id: FILTER_IS_IN_INBOX, filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true}, {id: FILTER_HAS_TAG, filtervar: "tags__id__all", datatype: "tag", multi: true},
{id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true}, {id: FILTER_DOES_NOT_HAVE_TAG, filtervar: "tags__id__none", datatype: "tag", multi: true},
{id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true}, {id: FILTER_HAS_ANY_TAG, filtervar: "is_tagged", datatype: "boolean", multi: false, default: true},
{id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false}, {id: FILTER_CREATED_BEFORE, filtervar: "created__date__lt", datatype: "date", multi: false},
{id: FILTER_CREATED_AFTER, name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false}, {id: FILTER_CREATED_AFTER, filtervar: "created__date__gt", datatype: "date", multi: false},
{id: FILTER_CREATED_YEAR, name: "Year created is", filtervar: "created__year", datatype: "number", multi: false}, {id: FILTER_CREATED_YEAR, filtervar: "created__year", datatype: "number", multi: false},
{id: FILTER_CREATED_MONTH, name: "Month created is", filtervar: "created__month", datatype: "number", multi: false}, {id: FILTER_CREATED_MONTH, filtervar: "created__month", datatype: "number", multi: false},
{id: FILTER_CREATED_DAY, name: "Day created is", filtervar: "created__day", datatype: "number", multi: false}, {id: FILTER_CREATED_DAY, filtervar: "created__day", datatype: "number", multi: false},
{id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false}, {id: FILTER_ADDED_BEFORE, filtervar: "added__date__lt", datatype: "date", multi: false},
{id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false}, {id: FILTER_ADDED_AFTER, filtervar: "added__date__gt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_BEFORE, filtervar: "modified__date__lt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_AFTER, filtervar: "modified__date__gt", datatype: "date", multi: false},
{id: FILTER_ASN_ISNULL, name: "ASN is null", filtervar: "archive_serial_number__isnull", datatype: "boolean", multi: false} {id: FILTER_ASN_ISNULL, filtervar: "archive_serial_number__isnull", datatype: "boolean", multi: false}
] ]
export interface FilterRuleType { export interface FilterRuleType {
id: number id: number
name: string
filtervar: string filtervar: string
isnull_filtervar?: string isnull_filtervar?: string
datatype: string //number, string, boolean, date datatype: string //number, string, boolean, date

@ -0,0 +1,8 @@
import { CustomDatePipe } from './custom-date.pipe';
describe('CustomDatePipe', () => {
it('create an instance', () => {
const pipe = new CustomDatePipe();
expect(pipe).toBeTruthy();
});
});

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