Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ba139810 | ||
|
|
27ff38b79e | ||
|
|
d521d7733b | ||
|
|
99bde7b613 | ||
|
|
cc5b7eb4e3 | ||
|
|
2b4a0efef8 | ||
|
|
9502917df6 | ||
|
|
6f3c72ef9a | ||
|
|
3915149ea1 | ||
|
|
f07fb6e1b3 | ||
|
|
e328c439ff | ||
|
|
caa9a340a4 | ||
|
|
b07537aa9a | ||
|
|
8e35e6c726 | ||
|
|
05a43a05a4 | ||
|
|
846a997323 | ||
|
|
e0231a4638 | ||
|
|
b4db388d58 | ||
|
|
e957d9aa53 | ||
|
|
72031824bf | ||
|
|
ae60fc8c2b | ||
|
|
cc3c7ad481 | ||
|
|
484edce5ab | ||
|
|
66aee1446c | ||
|
|
971392345b | ||
|
|
c5ae3377bd | ||
|
|
4ddd031976 | ||
|
|
c18180f344 | ||
|
|
c07bbcb33f | ||
|
|
142c2ac9ff | ||
|
|
88bf679fd9 | ||
|
|
a5de83fe7f | ||
|
|
63b4c33fb9 | ||
|
|
e0a1ba56dd | ||
|
|
aaa913f204 | ||
|
|
cf5747f0bd | ||
|
|
7df9e1f532 | ||
|
|
38d8b34ab4 | ||
|
|
24d07fcc17 | ||
|
|
14116c85e7 | ||
|
|
20b95fb506 | ||
|
|
3c25595fc2 | ||
|
|
af1034f84b | ||
|
|
cfc0925e04 | ||
|
|
746c03c198 | ||
|
|
baae3c2701 | ||
|
|
d88cd05857 | ||
|
|
73c80c3f7a | ||
|
|
35f9989255 | ||
|
|
33ddd033f2 | ||
|
|
b8fabb623e | ||
|
|
37c0fa4fab | ||
|
|
86ca37909b | ||
|
|
ce19525758 | ||
|
|
bb872ab56f | ||
|
|
ea342831da | ||
|
|
8d0ac690fb | ||
|
|
e0f14941e8 | ||
|
|
7cbb50565c | ||
|
|
274df28f39 | ||
|
|
4a6160869c | ||
|
|
87d969f44c | ||
|
|
7746ab4522 | ||
|
|
58cb42b44a | ||
|
|
8e330fc97a | ||
|
|
6a3ad77950 | ||
|
|
3d56621253 | ||
|
|
9e6cb1b868 | ||
|
|
321fad0564 | ||
|
|
eb7b767e35 | ||
|
|
98b26b8e16 | ||
|
|
176d1fa2eb | ||
|
|
072134366c | ||
|
|
afd92af168 | ||
|
|
2d61698509 | ||
|
|
5e286798d5 | ||
|
|
b7a825301a | ||
|
|
02da012603 | ||
|
|
4d99e72a0e | ||
|
|
26c0568b1e | ||
|
|
aa4d9782b7 | ||
|
|
43ccf6682c | ||
|
|
5e096bc8ce | ||
|
|
1e131f0217 | ||
|
|
321869edb0 | ||
|
|
c6e8faa718 | ||
|
|
6696c4d7a5 | ||
|
|
88254bdb79 | ||
|
|
4cc6bf889e |
No files matched your search
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Verify that all text files end in a trailing newline.
|
||||
|
||||
# Exit on first failing command.
|
||||
set -e
|
||||
|
||||
# Exit on unset variable.
|
||||
set -u
|
||||
|
||||
success=0
|
||||
|
||||
function is_plaintext_file() {
|
||||
local file="$1"
|
||||
if [[ $file == *.svg ]]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
file --brief "${file}" | grep text
|
||||
}
|
||||
|
||||
# Split strings on newlines.
|
||||
IFS='
|
||||
'
|
||||
for file in $(git ls-files)
|
||||
do
|
||||
if [[ -z $(is_plaintext_file "${file}") ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! [[ -z "$(tail -c 1 "${file}")" ]]; then
|
||||
printf "File must end in a trailing newline: %s\n" "${file}" >&2
|
||||
success=255
|
||||
fi
|
||||
done
|
||||
|
||||
exit "${success}"
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check for trailing whitespace at end of lines.
|
||||
|
||||
# Exit on first failing command.
|
||||
set -e
|
||||
# Exit on unset variable.
|
||||
set -u
|
||||
|
||||
FOUND_TRAILING_WHITESPACE=0
|
||||
|
||||
while read -r line; do
|
||||
if grep \
|
||||
"\s$" \
|
||||
--line-number \
|
||||
--with-filename \
|
||||
--binary-files=without-match \
|
||||
--exclude="*.svg" \
|
||||
--exclude="*.eps" \
|
||||
"${line}"; then
|
||||
echo "ERROR: Found trailing whitespace" >&2;
|
||||
FOUND_TRAILING_WHITESPACE=1
|
||||
fi
|
||||
done < <(git ls-files)
|
||||
|
||||
exit "${FOUND_TRAILING_WHITESPACE}"
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.7
|
||||
-
|
||||
name: Get pip cache dir
|
||||
id: pip-cache
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.7
|
||||
-
|
||||
name: Get pip cache dir
|
||||
id: pip-cache
|
||||
@@ -81,26 +81,12 @@ jobs:
|
||||
run: |
|
||||
cd src/
|
||||
pycodestyle
|
||||
whitespace:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Ensure there are no trailing spaces
|
||||
run: |
|
||||
.github/workflow-scripts/check-trailing-whitespace
|
||||
-
|
||||
name: Ensure all text files end with a trailing newline
|
||||
run: |
|
||||
.github/workflow-scripts/check-trailing-whitespace
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.7', '3.8', '3.9']
|
||||
python-version: ['3.6', '3.7', '3.8', '3.9']
|
||||
fail-fast: false
|
||||
steps:
|
||||
-
|
||||
@@ -136,7 +122,7 @@ jobs:
|
||||
pytest
|
||||
-
|
||||
name: Publish coverage results
|
||||
if: matrix.python-version == '3.9'
|
||||
if: matrix.python-version == '3.8'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# https://github.com/coveralls-clients/coveralls-python/issues/251
|
||||
@@ -172,7 +158,7 @@ jobs:
|
||||
path: src/documents/static/frontend/
|
||||
|
||||
build-release:
|
||||
needs: [frontend, documentation, tests, whitespace, codestyle]
|
||||
needs: [frontend, documentation, tests, codestyle]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
-
|
||||
@@ -182,7 +168,7 @@ jobs:
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.7
|
||||
-
|
||||
name: Install dependencies
|
||||
run: |
|
||||
@@ -282,7 +268,7 @@ jobs:
|
||||
build-docker-image:
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/ng-'))
|
||||
runs-on: ubuntu-latest
|
||||
needs: [frontend, tests, whitespace, codestyle]
|
||||
needs: [frontend, tests, codestyle]
|
||||
steps:
|
||||
-
|
||||
name: Prepare
|
||||
|
||||
+8
-3
@@ -8,7 +8,7 @@ RUN git clone https://github.com/agl/jbig2enc .
|
||||
RUN ./autogen.sh
|
||||
RUN ./configure && make
|
||||
|
||||
FROM python:3.9-slim-bullseye
|
||||
FROM python:3.7-slim
|
||||
|
||||
# Binary dependencies
|
||||
RUN apt-get update \
|
||||
@@ -33,11 +33,16 @@ RUN apt-get update \
|
||||
zlib1g \
|
||||
ghostscript \
|
||||
icc-profiles-free \
|
||||
# Mime type detection
|
||||
&& echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.list.d/bullseye.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
# fixes sudo / gosu issues
|
||||
libseccomp2 \
|
||||
# Mime type detection
|
||||
file \
|
||||
libmagic-dev \
|
||||
media-types \
|
||||
# OCRmyPDF dependencies
|
||||
# OCRmyPDF dependencies
|
||||
liblept5 \
|
||||
qpdf \
|
||||
tesseract-ocr \
|
||||
|
||||
@@ -14,14 +14,15 @@ django = "~=3.2"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=2.4.0"
|
||||
django-q = "~=1.3.4"
|
||||
django-q = "==1.3.4"
|
||||
djangorestframework = "~=3.12.2"
|
||||
filelock = "*"
|
||||
fuzzywuzzy = {extras = ["speedup"], version = "*"}
|
||||
gunicorn = "*"
|
||||
imap-tools = "*"
|
||||
langdetect = "*"
|
||||
numpy = "~=1.20.0"
|
||||
# numpy 1.20.0 drops python 3.6 support
|
||||
numpy = "~=1.19.5"
|
||||
pathvalidate = "*"
|
||||
pillow = "~=8.1"
|
||||
pikepdf = "~=2.5"
|
||||
@@ -33,11 +34,13 @@ psycopg2-binary = "*"
|
||||
redis = "*"
|
||||
# Pinned because aarch64 wheels and updates cause warnings when loading the classifier model.
|
||||
scikit-learn="==0.24.0"
|
||||
whitenoise = "~=5.3.0"
|
||||
watchdog = "~=2.1.0"
|
||||
# Prevent scipy updates because 1.6 is incompatible with python 3.6
|
||||
scipy="~=1.5.4"
|
||||
whitenoise = "~=5.2.0"
|
||||
watchdog = "~=1.0.0"
|
||||
whoosh="~=2.7.4"
|
||||
inotifyrecursive = "~=0.3.4"
|
||||
ocrmypdf = "~=12.3"
|
||||
ocrmypdf = "~=11.6"
|
||||
tqdm = "*"
|
||||
tika = "*"
|
||||
# TODO: This will sadly also install daphne+dependencies,
|
||||
@@ -47,10 +50,9 @@ channels-redis = "*"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
concurrent-log-handler = "*"
|
||||
# uvloop 0.15+ incompatible with python 3.6
|
||||
uvloop = "~=0.15"
|
||||
uvloop = "~=0.14.0"
|
||||
cryptography = "~=3.4"
|
||||
"pdfminer.six" = "*"
|
||||
"backports.zoneinfo" = "*"
|
||||
|
||||
[dev-packages]
|
||||
coveralls = "*"
|
||||
|
||||
Generated
+606
-703
File diff suppressed because it is too large.
Load diff
@@ -6,10 +6,6 @@
|
||||
[](https://hub.docker.com/r/jonaswinkler/paperless-ng)
|
||||
[](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master)
|
||||
|
||||
# Archive notice
|
||||
|
||||
This project is not dead! Rather, this project is now maintained as a community effort at https://github.com/paperless-ngx/paperless-ngx, a direct continuation of this project and drop-in replacement. Please see their project page and the documentation at https://docs.paperless-ngx.com/ for details.
|
||||
|
||||
# Paperless-ng
|
||||
|
||||
[Paperless (click me)](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
|
||||
@@ -66,7 +62,7 @@ If you want to see paperless-ng in action, [more screenshots are available in th
|
||||
|
||||
# Getting started
|
||||
|
||||
The recommended way to deploy paperless is docker-compose. The files in the /docker/compose directory are configured to pull the image from Docker Hub.
|
||||
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.
|
||||
|
||||
@@ -104,7 +100,6 @@ Paperless has been around a while now, and people are starting to build stuff on
|
||||
|
||||
* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. Updated to work with paperless-ng.
|
||||
* [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents.
|
||||
* [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for Paperless.
|
||||
|
||||
These projects also exist, but their status and compatibility with paperless-ng is unknown.
|
||||
|
||||
|
||||
+8
-10
@@ -12,29 +12,27 @@ Note that this role requires root access, so either run it in a playbook with a
|
||||
|
||||
- hosts: all
|
||||
roles:
|
||||
- role: paperless-ng
|
||||
- role: ansible
|
||||
become: yes
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
Most configuration variables from paperless-ng itself are available and accept their respective arguments.
|
||||
Every `PAPERLESS_*` configuration variable is lowercased and instead prefixed with `paperlessng_*` in `defaults/main.yml`.
|
||||
Every `PAPERLESS_*` configuration varaible is lowercased and instead prefixed with `paperlessng_*` in `defaults/main.yml`.
|
||||
|
||||
For a full listing including explanations and allowed values, see the current [documentation](https://paperless-ng.readthedocs.io/en/latest/configuration.html).
|
||||
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: latest
|
||||
paperlessng_version: 0.9.14
|
||||
|
||||
The [release](https://github.com/jonaswinkler/paperless-ng/releases) archive version of paperless-ng to install.
|
||||
`latest` stands for the latest release of paperless-ng.
|
||||
To install a specific version of paperless-ng, use the tag name of the release, e. g. `ng-1.4.4`, or specify a branch or commit id.
|
||||
|
||||
paperlessng_redis_host: localhost
|
||||
paperlessng_redis_port: 6379
|
||||
|
||||
Separate configuration values that combine into `PAPERLESS_REDIS`.
|
||||
Seperate configuration values that combine into `PAPERLESS_REDIS`.
|
||||
|
||||
paperlessng_db_type: sqlite
|
||||
|
||||
@@ -98,11 +96,11 @@ Example Playbook
|
||||
- hosts: all
|
||||
become: yes
|
||||
vars_files:
|
||||
- vars/paperless-ng.yml
|
||||
- vars/main.yml
|
||||
roles:
|
||||
- paperless-ng
|
||||
- ansible
|
||||
|
||||
`vars/paperless-ng.yml`:
|
||||
`vars/main.yml`:
|
||||
|
||||
paperlessng_media_root: /mnt/media/smbshare
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
- regexp: PAPERLESS_TIKA_ENDPOINT
|
||||
line: "PAPERLESS_TIKA_ENDPOINT={{ paperlessng_tika_endpoint }}"
|
||||
- regexp: PAPERLESS_TIKA_GOTENBERG_ENDPOINT
|
||||
line: "PAPERLESS_TIKA_GOTENBERG_ENDPOINT={{ paperlessng_tika_gotenberg_endpoint }}"
|
||||
line: "PAPERLESS_TIKA_GOTENBERG_ENDPOINT={{ paperlessng_tika_endpoint }}"
|
||||
# Software tweaks
|
||||
- regexp: PAPERLESS_TIME_ZONE
|
||||
line: "PAPERLESS_TIME_ZONE={{ paperlessng_time_zone }}"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
COMPOSE_PROJECT_NAME=paperless
|
||||
COMPOSE_PROJECT_NAME=paperless
|
||||
+38
-38
@@ -4,37 +4,37 @@ set -e
|
||||
|
||||
# Source: https://github.com/sameersbn/docker-gitlab/
|
||||
map_uidgid() {
|
||||
USERMAP_ORIG_UID=$(id -u paperless)
|
||||
USERMAP_ORIG_GID=$(id -g paperless)
|
||||
USERMAP_NEW_UID=${USERMAP_UID:-$USERMAP_ORIG_UID}
|
||||
USERMAP_NEW_GID=${USERMAP_GID:-${USERMAP_ORIG_GID:-$USERMAP_NEW_UID}}
|
||||
if [[ ${USERMAP_NEW_UID} != "${USERMAP_ORIG_UID}" || ${USERMAP_NEW_GID} != "${USERMAP_ORIG_GID}" ]]; then
|
||||
echo "Mapping UID and GID for paperless:paperless to $USERMAP_NEW_UID:$USERMAP_NEW_GID"
|
||||
usermod -u "${USERMAP_NEW_UID}" paperless
|
||||
groupmod -o -g "${USERMAP_NEW_GID}" paperless
|
||||
fi
|
||||
USERMAP_ORIG_UID=$(id -u paperless)
|
||||
USERMAP_ORIG_GID=$(id -g paperless)
|
||||
USERMAP_NEW_UID=${USERMAP_UID:-$USERMAP_ORIG_UID}
|
||||
USERMAP_NEW_GID=${USERMAP_GID:-${USERMAP_ORIG_GID:-$USERMAP_NEW_UID}}
|
||||
if [[ ${USERMAP_NEW_UID} != "${USERMAP_ORIG_UID}" || ${USERMAP_NEW_GID} != "${USERMAP_ORIG_GID}" ]]; then
|
||||
echo "Mapping UID and GID for paperless:paperless to $USERMAP_NEW_UID:$USERMAP_NEW_GID"
|
||||
usermod -u "${USERMAP_NEW_UID}" paperless
|
||||
groupmod -o -g "${USERMAP_NEW_GID}" paperless
|
||||
fi
|
||||
}
|
||||
|
||||
initialize() {
|
||||
map_uidgid
|
||||
|
||||
for dir in export data data/index media media/documents media/documents/originals media/documents/thumbnails; do
|
||||
if [[ ! -d "../$dir" ]]; then
|
||||
echo "Creating directory ../$dir"
|
||||
if [[ ! -d "../$dir" ]]
|
||||
then
|
||||
echo "creating directory ../$dir"
|
||||
mkdir ../$dir
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Creating directory /tmp/paperless"
|
||||
echo "creating directory /tmp/paperless"
|
||||
mkdir -p /tmp/paperless
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
set +e
|
||||
chown -R paperless:paperless ../
|
||||
chown -R paperless:paperless /tmp/paperless
|
||||
find .. -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} +
|
||||
set -e
|
||||
set -e
|
||||
|
||||
gosu paperless /sbin/docker-prepare.sh
|
||||
gosu paperless /sbin/docker-prepare.sh
|
||||
}
|
||||
|
||||
install_languages() {
|
||||
@@ -50,35 +50,35 @@ install_languages() {
|
||||
apt-get update
|
||||
|
||||
for lang in "${langs[@]}"; do
|
||||
pkg="tesseract-ocr-$lang"
|
||||
# English is installed by default
|
||||
#if [[ "$lang" == "eng" ]]; then
|
||||
# continue
|
||||
#fi
|
||||
pkg="tesseract-ocr-$lang"
|
||||
# English is installed by default
|
||||
#if [[ "$lang" == "eng" ]]; then
|
||||
# continue
|
||||
#fi
|
||||
|
||||
if dpkg -s $pkg &>/dev/null; then
|
||||
echo "Package $pkg already installed!"
|
||||
continue
|
||||
fi
|
||||
if dpkg -s $pkg &> /dev/null; then
|
||||
echo "package $pkg already installed!"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! apt-cache show $pkg &>/dev/null; then
|
||||
echo "Package $pkg not found! :("
|
||||
continue
|
||||
fi
|
||||
if ! apt-cache show $pkg &> /dev/null; then
|
||||
echo "package $pkg not found! :("
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Installing package $pkg..."
|
||||
if ! apt-get -y install "$pkg" &>/dev/null; then
|
||||
echo "Could not install $pkg"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Installing package $pkg..."
|
||||
if ! apt-get -y install "$pkg" &> /dev/null; then
|
||||
echo "Could not install $pkg"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo "Paperless-ng docker container starting..."
|
||||
|
||||
# Install additional languages if specified
|
||||
if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then
|
||||
install_languages "$PAPERLESS_OCR_LANGUAGES"
|
||||
if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then
|
||||
install_languages "$PAPERLESS_OCR_LANGUAGES"
|
||||
fi
|
||||
|
||||
initialize
|
||||
|
||||
+11
-6
@@ -149,15 +149,22 @@ Ansible Route
|
||||
|
||||
Most of the update process is automated when using the ansible role.
|
||||
|
||||
1. Update the role to the target release tag to make sure the ansible scripts are compatible:
|
||||
1. Backup your defined role variables file outside the paperless source-tree:
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
$ ansible-galaxy install git+https://github.com/jonaswinkler/paperless-ng.git,master --force
|
||||
$ cp ansible/vars.yml ~/vars.yml.old
|
||||
|
||||
2. Update the role variable definitions ``vars/paperless-ng.yml`` (where appropriate).
|
||||
2. Pull the release tag you want to update to:
|
||||
|
||||
3. Run the ansible playbook you created created during :ref:`installation <setup-ansible>` again:
|
||||
.. 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::
|
||||
|
||||
@@ -240,8 +247,6 @@ Document exporter
|
||||
The document exporter exports all your data from paperless into a folder for
|
||||
backup or migration to another DMS.
|
||||
|
||||
If you use the document exporter within a cronjob to backup your data you might use the ``-T`` flag behind exec to suppress "The input device is not a TTY" errors. For example: ``docker-compose exec -T webserver document_exporter ../export``
|
||||
|
||||
.. code::
|
||||
|
||||
document_exporter target [-c] [-f] [-d]
|
||||
|
||||
+11
-53
@@ -5,56 +5,6 @@
|
||||
Changelog
|
||||
*********
|
||||
|
||||
paperless-ng 1.5.0
|
||||
##################
|
||||
|
||||
Support for Python 3.6 was dropped.
|
||||
|
||||
* Updated python dependencies.
|
||||
* Base image of the docker image changed from Debian Buster to Debian Bullseye due to its recent release.
|
||||
* The docker image now uses python 3.9.
|
||||
* Added the Luxembourgish locale. Thanks for translating!
|
||||
* `Daniel Albers`_ added support for making the files and folders ignored by the paperless consume folder scanner configurable. See ``PAPERLESS_CONSUMER_IGNORE_PATTERNS``.
|
||||
|
||||
paperless-ng 1.4.5
|
||||
##################
|
||||
|
||||
This is a maintenance release.
|
||||
|
||||
* Updated Python and Angular dependencies.
|
||||
* Changed the algorithm that changes permissions during startup. This is still fast,
|
||||
and will hopefully cause less issues.
|
||||
* Fixed an issue that would sometimes cause paperless to write an incomplete
|
||||
classification model file to disk.
|
||||
* Fixed an issue with the OCRmyPDF parser that would always try to extract text
|
||||
with PDFminer even from non-PDF files.
|
||||
|
||||
paperless-ng 1.4.4
|
||||
##################
|
||||
|
||||
* Drastically decreased the startup time of the docker container. The startup script adjusts file permissions of all data only if changes are required.
|
||||
* Paperless mail: Added ability to specify the character set for each server.
|
||||
* Document consumption: Ignore Mac OS specific files such as ``.DS_STORE`` and ``._XXXXX.pdf``.
|
||||
* Fixed an issue with the automatic matching algorithm that prevents paperless from consuming new files.
|
||||
* Updated translations.
|
||||
|
||||
paperless-ng 1.4.3
|
||||
##################
|
||||
|
||||
* Additions and changes
|
||||
|
||||
* Added Swedish locale.
|
||||
* `Stéphane Brunner`_ added an option to disable the progress bars of all management commands.
|
||||
* `Jo Vandeginste`_ added support for RTF documents to the Apache TIKA parser.
|
||||
* `Michael Shamoon`_ added dark mode for the login and logout pages.
|
||||
* `Alexander Menk`_ added additional stylesheets for printing. You can now print any page of paperless and the print result will hide the page header, sidebar, and action buttons.
|
||||
* Added support for sorting when using full text search.
|
||||
|
||||
* Fixes
|
||||
|
||||
* `puuu`_ fixed ``PAPERLESS_FORCE_SCRIPT_NAME``. You can now host paperless on sub paths such as ``https://localhost:8000/paperless/``.
|
||||
* Fixed an issue with the document consumer crashing on certain documents due to issues with pdfminer.six. This library is used for PDF text extraction.
|
||||
|
||||
paperless-ng 1.4.2
|
||||
##################
|
||||
|
||||
@@ -109,6 +59,17 @@ paperless-ng 1.4.0
|
||||
To do this, execute the ``document_index reindex`` management command
|
||||
(see :ref:`administration-index`).
|
||||
|
||||
.. note::
|
||||
|
||||
Some packages that paperless depends on are slowly dropping Python 3.6
|
||||
support one after another, including the web server. Supporting Python
|
||||
3.6 means that I cannot update these packages anymore.
|
||||
|
||||
At some point, paperless will drop Python 3.6 support. If using a bare
|
||||
metal installation and you're still on Python 3.6, upgrade to 3.7 or newer.
|
||||
|
||||
If using docker, this does not affect you.
|
||||
|
||||
paperless-ng 1.3.2
|
||||
##################
|
||||
|
||||
@@ -1405,8 +1366,6 @@ bulk of the work on this big change.
|
||||
|
||||
* Initial release
|
||||
|
||||
.. _Alexander Menk: https://github.com/amenk
|
||||
.. _puuu: https://github.com/puuu
|
||||
.. _WhiteHatTux: https://github.com/WhiteHatTux
|
||||
.. _Chris Nagy: https://github.com/what-name
|
||||
.. _sbrl: https://github.com/sbrl
|
||||
@@ -1482,7 +1441,6 @@ bulk of the work on this big change.
|
||||
.. _JOKer: https://github.com/MasterofJOKers
|
||||
.. _Brian Cribbs: https://github.com/cribbstechnolog
|
||||
.. _Brendan M. Sleight: https://github.com/bmsleight
|
||||
.. _Daniel Albers: https://github.com/AlD
|
||||
|
||||
.. _#20: https://github.com/the-paperless-project/paperless/issues/20
|
||||
.. _#44: https://github.com/the-paperless-project/paperless/issues/44
|
||||
|
||||
+15
-34
@@ -154,6 +154,10 @@ PAPERLESS_FORCE_SCRIPT_NAME=<path>
|
||||
To host paperless under a subpath url like example.com/paperless you set
|
||||
this value to /paperless. No trailing slash!
|
||||
|
||||
.. note::
|
||||
|
||||
I don't know if this works in paperless-ng. Probably not.
|
||||
|
||||
Defaults to none, which hosts paperless at "/".
|
||||
|
||||
PAPERLESS_STATIC_URL=<path>
|
||||
@@ -174,11 +178,11 @@ PAPERLESS_AUTO_LOGIN_USERNAME=<username>
|
||||
Defaults to none, which disables this feature.
|
||||
|
||||
PAPERLESS_ADMIN_USER=<username>
|
||||
If this environment variable is specified, Paperless automatically creates
|
||||
a superuser with the provided username at start. This is useful in cases
|
||||
where you can not run the `createsuperuser` command seperately, such as Kubernetes
|
||||
If this environment variable is specified, Paperless automatically creates
|
||||
a superuser with the provided username at start. This is useful in cases
|
||||
where you can not run the `createsuperuser` command seperately, such as Kubernetes
|
||||
or AWS ECS.
|
||||
|
||||
|
||||
Requires `PAPERLESS_ADMIN_PASSWORD` to be set.
|
||||
|
||||
.. note::
|
||||
@@ -188,7 +192,7 @@ PAPERLESS_ADMIN_USER=<username>
|
||||
the lifecycle of the containers.
|
||||
|
||||
PAPERLESS_ADMIN_MAIL=<email>
|
||||
(Optional) Specify superuser email address. Only used when
|
||||
(Optional) Specify superuser email address. Only used when
|
||||
`PAPERLESS_ADMIN_USER` is set.
|
||||
|
||||
Defaults to ``root@localhost``.
|
||||
@@ -222,24 +226,17 @@ PAPERLESS_ENABLE_HTTP_REMOTE_USER=<bool>
|
||||
Also see the warning `in the official documentation <https://docs.djangoproject.com/en/3.1/howto/auth-remote-user/#configuration>`.
|
||||
|
||||
Defaults to `false` which disables this feature.
|
||||
|
||||
|
||||
PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=<str>
|
||||
If `PAPERLESS_ENABLE_HTTP_REMOTE_USER` is enabled, this property allows to
|
||||
customize the name of the HTTP header from which the authenticated username
|
||||
If `PAPERLESS_ENABLE_HTTP_REMOTE_USER` is enabled, this property allows to
|
||||
customize the name of the HTTP header from which the authenticated username
|
||||
is extracted. Values are in terms of
|
||||
[HttpRequest.META](https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.META).
|
||||
Thus, the configured value must start with `HTTP_` followed by the
|
||||
Thus, the configured value must start with `HTTP_` followed by the
|
||||
normalized actual header name.
|
||||
|
||||
|
||||
Defaults to `HTTP_REMOTE_USER`.
|
||||
|
||||
PAPERLESS_LOGOUT_REDIRECT_URL=<str>
|
||||
URL to redirect the user to after a logout. This can be used together with
|
||||
`PAPERLESS_ENABLE_HTTP_REMOTE_USER` to redirect the user back to the SSO
|
||||
application's logout page.
|
||||
|
||||
Defaults to None, which disables this feature.
|
||||
|
||||
|
||||
.. _configuration-ocr:
|
||||
|
||||
OCR settings
|
||||
@@ -265,8 +262,6 @@ PAPERLESS_OCR_LANGUAGE=<lang>
|
||||
|
||||
Defaults to "eng".
|
||||
|
||||
Note: If your language contains a '-' such as chi-sim, you must use chi_sim
|
||||
|
||||
PAPERLESS_OCR_MODE=<mode>
|
||||
Tell paperless when and how to perform ocr on your documents. Four modes
|
||||
are available:
|
||||
@@ -631,20 +626,6 @@ PAPERLESS_IGNORE_DATES=<string>
|
||||
|
||||
Defaults to an empty string to not ignore any dates.
|
||||
|
||||
PAPERLESS_DATE_ORDER=<format>
|
||||
Paperless will try to determine the document creation date from its contents.
|
||||
Specify the date format Paperless should expect to see within your documents.
|
||||
|
||||
This option defaults to DMY which translates to day first, month second, and year
|
||||
last order. Characters D, M, or Y can be shuffled to meet the required order.
|
||||
|
||||
PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>
|
||||
By default, paperless ignores certain files and folders in the consumption
|
||||
directory, such as system files created by the Mac OS.
|
||||
|
||||
This can be adjusted by configuring a custom json array with patterns to exclude.
|
||||
|
||||
Defautls to ``[".DS_STORE/*", "._*", ".stfolder/*"]``.
|
||||
|
||||
Binaries
|
||||
########
|
||||
|
||||
@@ -6,7 +6,7 @@ Contributing to Paperless
|
||||
.. warning::
|
||||
|
||||
This section is not updated to paperless-ng yet.
|
||||
|
||||
|
||||
Maybe you've been using Paperless for a while and want to add a feature or two,
|
||||
or maybe you've come across a bug that you have some ideas how to solve. The
|
||||
beauty of Free software is that you can see what's wrong and help to get it
|
||||
|
||||
+9
-9
@@ -23,10 +23,10 @@ Apart from that, the folder structure is as follows:
|
||||
* ``scripts/`` - Various scripts that help with different parts of development.
|
||||
* ``docker/`` - Files required to build the docker image.
|
||||
|
||||
Initial setup and first start
|
||||
Initial setup and first start
|
||||
=============================
|
||||
|
||||
After you forked and cloned the code from github you need to perform a first-time setup.
|
||||
After you forked and cloned the code from github you need to perform a first-time setup.
|
||||
To do the setup you need to perform the steps from the following chapters in a certain order:
|
||||
|
||||
1. Install prerequisites + pipenv as mentioned in :ref:`Bare metal route <setup-bare_metal>`
|
||||
@@ -35,19 +35,19 @@ To do the setup you need to perform the steps from the following chapters in a c
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
$ npm install -g @angular/cli
|
||||
$ npm install -g @angular/cli
|
||||
|
||||
4. Create ``consume`` and ``media`` folders in the cloned root folder.
|
||||
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
mkdir -p consume media
|
||||
|
||||
5. You can now either ...
|
||||
|
||||
* install redis or
|
||||
* install redis or
|
||||
* use the included scripts/start-services.sh to use docker to fire up a redis instance (and some other services such as tika, gotenberg and a postgresql server) or
|
||||
* spin up a bare redis container
|
||||
* spin up a bare redis container
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
@@ -79,7 +79,7 @@ To do the setup you need to perform the steps from the following chapters in a c
|
||||
python3 manage.py runserver & python3 manage.py document_consumer & python3 manage.py qcluster
|
||||
|
||||
10. Login with the superuser credentials provided in step 8 at ``http://localhost:8000`` to create a session that enables you to use the backend.
|
||||
|
||||
|
||||
Backend development environment is now ready, to start Frontend development go to ``/src-ui`` and run ``ng serve``. From there you can use ``http://localhost:4200`` for a preview.
|
||||
|
||||
Back end development
|
||||
@@ -207,7 +207,7 @@ Adding new languages requires adding the translated files in the "src-ui/src/loc
|
||||
// Add your new language here
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
``dateInputFormat`` is a special string that defines the behavior of the date input fields and absolutely needs to contain "dd", "mm" and "yyyy".
|
||||
|
||||
3. Import and register the Angular data for this locale in "src-ui/src/app/app.module.ts":
|
||||
@@ -320,7 +320,7 @@ methods ``parse`` and ``get_thumbnail``. You can provide your own implementation
|
||||
|
||||
# The content of the document.
|
||||
self.text = "content"
|
||||
|
||||
|
||||
# Optional: path to a PDF document that you created from the original.
|
||||
self.archive_path = os.path.join(self.tempdir, "archived.pdf")
|
||||
|
||||
|
||||
+2
-2
@@ -68,10 +68,10 @@ reuse the text. The web interface is a lot snappier, since it runs
|
||||
in your browser and paperless has to do much less work to serve the data.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
You can adjust some of the settings so that paperless uses less processing
|
||||
power. See :ref:`setup-less_powerful_devices` for details.
|
||||
|
||||
|
||||
|
||||
**Q:** *How do I install paperless-ng on Raspberry Pi?*
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ resources in the documentation:
|
||||
paperless-ng.
|
||||
* Paperless is now integrated with a
|
||||
:ref:`task processing queue <setup-task_processor>` that tells you
|
||||
at a glance when and why something is not working.
|
||||
at a glance when and why something is not working.
|
||||
* The :ref:`changelog <paperless_changelog>` contains a detailed list of all changes
|
||||
in paperless-ng.
|
||||
|
||||
|
||||
+25
-35
@@ -13,48 +13,42 @@ that works right for you based on recommendations from other Paperless users.
|
||||
Physical scanners
|
||||
=================
|
||||
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brand | Model | Supports | Recommended By |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| | | FTP | NFS | SMB | SMTP | API [1]_ | |
|
||||
+=========+================+=====+=====+=====+======+==========+================+
|
||||
| Brother | `ADS-1700W`_ | yes | no | yes | yes | |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `ADS-1600W`_ | yes | no | yes | yes | |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `ADS-1500W`_ | yes | no | yes | yes | |`danielquinn`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `ADS-1100W`_ | yes | no | no | no | |`ytzelf`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `MFC-J6930DW`_ | yes | | | | |`ayounggun`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `MFC-L5850DW`_ | yes | | | yes | |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `MFC-J5910DW`_ | yes | | | | |`bmsleight`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Brother | `MFC-9142CDN`_ | yes | | yes | | |`REOLDEV`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Fujitsu | `ix500`_ | yes | | yes | | |`eonist`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Epson | `WF-7710DWF`_ | yes | | yes | | |`Skylinar`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Fujitsu | `S1300i`_ | yes | | yes | | |`jonaswinkler`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
| Doxie | `Q2`_ | no | no | no | no | yes |`Unkn0wnCat`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------+----------------+
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brand | Model | Supports | Recommended By |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| | | FTP | NFS | SMB | SMTP | |
|
||||
+=========+================+=====+=====+=====+======+================+
|
||||
| Brother | `ADS-1700W`_ | yes | no | yes | yes |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `ADS-1600W`_ | yes | no | yes | yes |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `ADS-1500W`_ | yes | no | yes | yes |`danielquinn`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-J6930DW`_ | yes | | | |`ayounggun`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-L5850DW`_ | yes | | | yes |`holzhannes`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-J5910DW`_ | yes | | | |`bmsleight`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Brother | `MFC-9142CDN`_ | yes | | yes | |`REOLDEV`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Fujitsu | `ix500`_ | yes | | yes | |`eonist`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Epson | `WF-7710DWF`_ | yes | | yes | |`Skylinar`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
| Fujitsu | `S1300i`_ | yes | | yes | |`jonaswinkler`_ |
|
||||
+---------+----------------+-----+-----+-----+------+----------------+
|
||||
|
||||
.. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw
|
||||
.. _ADS-1700W: https://www.brother-usa.com/products/ads1700w
|
||||
.. _ADS-1600W: https://www.brother-usa.com/products/ads1600w
|
||||
.. _ADS-1500W: https://www.brother.ca/en/p/ads1500w
|
||||
.. _ADS-1100W: https://support.brother.com/g/b/downloadtop.aspx?c=fr&lang=fr&prod=ads1100w_eu_as_cn
|
||||
.. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
|
||||
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
|
||||
.. _MFC-9142CDN: https://www.brother.co.uk/printers/laser-printers/mfc9140cdn
|
||||
.. _ix500: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
|
||||
.. _WF-7710DWF: https://www.epson.de/en/products/printers/inkjet-printers/for-home/workforce-wf-7710dwf
|
||||
.. _S1300i: https://www.fujitsu.com/global/products/computing/peripheral/scanners/soho/s1300i/
|
||||
.. _Q2: https://www.getdoxie.com/product/doxie-q/
|
||||
|
||||
|
||||
.. _danielquinn: https://github.com/danielquinn
|
||||
@@ -65,10 +59,6 @@ Physical scanners
|
||||
.. _Skylinar: https://github.com/Skylinar
|
||||
.. _jonaswinkler: https://github.com/jonaswinkler
|
||||
.. _holzhannes: https://github.com/holzhannes
|
||||
.. _ytzelf: https://github.com/ytzelf
|
||||
.. _Unkn0wnCat: https://github.com/Unkn0wnCat
|
||||
|
||||
.. [1] Scanners with API Integration allow to push scanned documents directly to :ref:`Paperless API <api-file_uploads>`, sometimes referred to as Webhook or Document POST.
|
||||
|
||||
Mobile phone software
|
||||
=====================
|
||||
|
||||
+6
-26
@@ -116,7 +116,9 @@ performs all the steps described in :ref:`setup-docker_hub` automatically.
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
$ curl -L https://raw.githubusercontent.com/jonaswinkler/paperless-ng/master/install-paperless-ng.sh | bash
|
||||
$ wget https://raw.githubusercontent.com/jonaswinkler/paperless-ng/master/install-paperless-ng.sh
|
||||
$ chmod +x install-paperless-ng.sh
|
||||
$ ./install-paperless-ng.sh
|
||||
|
||||
.. _setup-docker_hub:
|
||||
|
||||
@@ -171,24 +173,6 @@ Install Paperless from Docker Hub
|
||||
|
||||
Don't change the part after the colon or paperless wont find your documents.
|
||||
|
||||
You may also need to change the default port that the webserver will use
|
||||
from the default (8000):
|
||||
|
||||
.. code::
|
||||
|
||||
ports:
|
||||
- 8000:8000
|
||||
|
||||
Replace the part BEFORE the colon with a port of your choice:
|
||||
|
||||
.. code::
|
||||
|
||||
ports:
|
||||
- 8010:8000
|
||||
|
||||
Don't change the part after the colon or edit other lines that refer to
|
||||
port 8000. Modifying the part before the colon will map requests on another
|
||||
port to the webserver running on the default port.
|
||||
|
||||
5. Modify ``docker-compose.env``, following the comments in the file. The
|
||||
most important change is to set ``USERMAP_UID`` and ``USERMAP_GID``
|
||||
@@ -302,7 +286,7 @@ writing. Windows is not and will never be supported.
|
||||
|
||||
Use this list for your preferred package management:
|
||||
|
||||
.. code::
|
||||
.. code::
|
||||
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation optipng gnupg libpq-dev libmagic-dev mime-support
|
||||
|
||||
@@ -321,7 +305,7 @@ writing. Windows is not and will never be supported.
|
||||
|
||||
Use this list for your preferred package management:
|
||||
|
||||
.. code::
|
||||
.. code::
|
||||
|
||||
unpaper ghostscript icc-profiles-free qpdf liblept5 libxml2 pngquant zlib1g tesseract-ocr
|
||||
|
||||
@@ -376,11 +360,7 @@ writing. Windows is not and will never be supported.
|
||||
Adjust as necessary if you configured different folders.
|
||||
|
||||
8. Install python requirements from the ``requirements.txt`` file.
|
||||
It is up to you if you wish to use a virtual environment or not. First you should update your pip, so it gets the actual packages.
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
sudo -Hu paperless pip3 install --upgrade pip
|
||||
It is up to you if you wish to use a virtual environment or not.
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ UserWarning in sklearn on every single document
|
||||
You may encounter warnings like this:
|
||||
|
||||
.. code::
|
||||
|
||||
|
||||
/usr/local/lib/python3.7/site-packages/sklearn/base.py:315:
|
||||
UserWarning: Trying to unpickle estimator CountVectorizer from version 0.23.2 when using version 0.24.0.
|
||||
This might lead to breaking code or invalid results. Use at your own risk.
|
||||
@@ -200,13 +200,13 @@ This might have multiple reasons.
|
||||
File "/usr/local/lib/python3.7/site-packages/gunicorn/http/wsgi.py", line 386, in sendfile
|
||||
sent += os.sendfile(sockno, fileno, offset + sent, count)
|
||||
OSError: [Errno 22] Invalid argument
|
||||
|
||||
|
||||
To fix this issue, add
|
||||
|
||||
.. code::
|
||||
|
||||
SENDFILE=0
|
||||
|
||||
|
||||
to your `docker-compose.env` file.
|
||||
|
||||
Error while reading metadata
|
||||
|
||||
@@ -71,7 +71,7 @@ your documents:
|
||||
|
||||
This process can be configured to fit your needs. If you don't want paperless
|
||||
to create archived versions for digital documents, you can configure that by
|
||||
configuring ``PAPERLESS_OCR_MODE=skip_noarchive``. Please read the
|
||||
configuring ``PAPERLESS_OCR_MODE=skip_noarchive``. Please read the
|
||||
:ref:`relevant section in the documentation <configuration-ocr>`.
|
||||
|
||||
.. note::
|
||||
@@ -289,7 +289,7 @@ Matching specific tags, correspondents or types:
|
||||
Matching dates:
|
||||
|
||||
.. code::
|
||||
|
||||
|
||||
created:[2005 to 2009]
|
||||
added:yesterday
|
||||
modified:today
|
||||
@@ -306,11 +306,11 @@ Matching inexact words:
|
||||
auto complete and query correction.
|
||||
|
||||
All of these constructs can be combined as you see fit.
|
||||
If you want to learn more about the query language used by paperless, paperless uses Whoosh's default query language.
|
||||
If you want to learn more about the query language used by paperless, paperless uses Whoosh's default query language.
|
||||
Head over to `Whoosh query language <https://whoosh.readthedocs.io/en/latest/querylang.html>`_.
|
||||
For details on what date parsing utilities are available, see
|
||||
`Date parsing <https://whoosh.readthedocs.io/en/latest/dates.html#parsing-date-queries>`_.
|
||||
|
||||
|
||||
|
||||
.. _usage-recommended_workflow:
|
||||
|
||||
@@ -385,7 +385,7 @@ Once you have scanned in a document, proceed in paperless as follows.
|
||||
6. Remove inbox tags from the documents.
|
||||
|
||||
.. hint::
|
||||
|
||||
|
||||
You can setup manual matching rules for your correspondents and tags and
|
||||
paperless will assign them automatically. After consuming a couple documents,
|
||||
you can even ask paperless to *learn* when to assign tags and correspondents
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import os
|
||||
|
||||
bind = '0.0.0.0:8000'
|
||||
workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 2))
|
||||
worker_class = 'paperless.workers.ConfigurableWorker'
|
||||
worker_class = 'uvicorn.workers.UvicornWorker'
|
||||
timeout = 120
|
||||
|
||||
def pre_fork(server, worker):
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
#PAPERLESS_CONSUMER_POLLING=10
|
||||
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
|
||||
#PAPERLESS_CONSUMER_RECURSIVE=false
|
||||
#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*"]
|
||||
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
|
||||
#PAPERLESS_OPTIMIZE_THUMBNAILS=true
|
||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||
|
||||
+45
-48
@@ -8,96 +8,93 @@
|
||||
-i https://pypi.python.org/simple
|
||||
--extra-index-url https://www.piwheels.org/simple
|
||||
aioredis==1.3.1
|
||||
arrow==1.1.1; python_version >= '3.6'
|
||||
asgiref==3.4.1; python_version >= '3.6'
|
||||
arrow==1.0.3; python_version >= '3.6'
|
||||
asgiref==3.3.4; python_version >= '3.6'
|
||||
async-timeout==3.0.1; python_full_version >= '3.5.3'
|
||||
attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
attrs==20.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
autobahn==21.3.1; python_version >= '3.7'
|
||||
automat==20.2.0
|
||||
backports.zoneinfo==0.2.1
|
||||
blessed==1.18.1; python_version >= '2.7'
|
||||
certifi==2021.5.30
|
||||
cffi==1.14.6
|
||||
channels-redis==3.3.0
|
||||
channels==3.0.4
|
||||
blessed==1.18.0
|
||||
certifi==2020.12.5
|
||||
cffi==1.14.5
|
||||
channels-redis==3.2.0
|
||||
channels==3.0.3
|
||||
chardet==4.0.0; python_version >= '3.1'
|
||||
charset-normalizer==2.0.4; python_version >= '3'
|
||||
click==8.0.1; python_version >= '3.6'
|
||||
coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
coloredlogs==15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
concurrent-log-handler==0.9.19
|
||||
constantly==15.1.0
|
||||
cryptography==3.4.7
|
||||
daphne==3.0.2; python_version >= '3.6'
|
||||
dateparser==1.0.0
|
||||
django-cors-headers==3.8.0
|
||||
django-extensions==3.1.3
|
||||
django-cors-headers==3.7.0
|
||||
django-extensions==3.1.2
|
||||
django-filter==2.4.0
|
||||
django-picklefield==3.0.1; python_version >= '3'
|
||||
django-q==1.3.9
|
||||
django==3.2.6
|
||||
django-q==1.3.4
|
||||
django==3.2
|
||||
djangorestframework==3.12.4
|
||||
filelock==3.0.12
|
||||
fuzzywuzzy[speedup]==0.18.0
|
||||
gunicorn==20.1.0
|
||||
h11==0.12.0; python_version >= '3.6'
|
||||
hiredis==2.0.0; python_version >= '3.6'
|
||||
httptools==0.2.0
|
||||
humanfriendly==9.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
httptools==0.1.1
|
||||
humanfriendly==9.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
hyperlink==21.0.0
|
||||
idna==3.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
imap-tools==0.46.0
|
||||
img2pdf==0.4.1
|
||||
idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
imap-tools==0.39.0
|
||||
img2pdf==0.4.0
|
||||
incremental==21.3.0
|
||||
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
inotifyrecursive==0.3.5
|
||||
joblib==1.0.1; python_version >= '3.6'
|
||||
langdetect==1.0.9
|
||||
langdetect==1.0.8
|
||||
lxml==4.6.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
msgpack==1.0.2
|
||||
numpy==1.20.3
|
||||
ocrmypdf==12.3.2
|
||||
numpy==1.19.5
|
||||
ocrmypdf==11.7.3
|
||||
pathvalidate==2.4.1
|
||||
pdfminer.six==20201018
|
||||
pikepdf==2.16.1
|
||||
pillow==8.3.1
|
||||
pikepdf==2.11.1
|
||||
pillow==8.2.0
|
||||
pluggy==0.13.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
portalocker==2.3.0; python_version >= '3'
|
||||
psycopg2-binary==2.9.1
|
||||
psycopg2-binary==2.8.6
|
||||
pyasn1-modules==0.2.8
|
||||
pyasn1==0.4.8
|
||||
pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
pyopenssl==20.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
python-dateutil==2.8.2
|
||||
python-dotenv==0.19.0
|
||||
python-dateutil==2.8.1
|
||||
python-dotenv==0.17.0
|
||||
python-gnupg==0.4.7
|
||||
python-levenshtein==0.12.2
|
||||
python-magic==0.4.24
|
||||
python-magic==0.4.22
|
||||
pytz==2021.1
|
||||
pyyaml==5.4.1
|
||||
redis==3.5.3
|
||||
regex==2021.8.3
|
||||
reportlab==3.6.1; python_version >= '2.7' and python_version < '4'
|
||||
requests==2.26.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
regex==2021.4.4
|
||||
reportlab==3.5.67; python_version >= '2.7' and python_version < '4'
|
||||
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.7.1; python_version < '3.10' and python_version >= '3.7'
|
||||
service-identity==21.1.0
|
||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
sortedcontainers==2.4.0
|
||||
scipy==1.5.4
|
||||
service-identity==18.1.0
|
||||
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.2.0; python_version >= '3.6'
|
||||
threadpoolctl==2.1.0; python_version >= '3.5'
|
||||
tika==1.24
|
||||
tqdm==4.62.1
|
||||
twisted[tls]==21.7.0; python_full_version >= '3.6.7'
|
||||
tqdm==4.60.0
|
||||
twisted[tls]==21.2.0; python_full_version >= '3.5.4'
|
||||
txaio==21.2.1; python_version >= '3.6'
|
||||
typing-extensions==3.10.0.0
|
||||
tzlocal==3.0; python_version >= '3.6'
|
||||
urllib3==1.26.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
uvicorn[standard]==0.15.0
|
||||
uvloop==0.16.0
|
||||
watchdog==2.1.3
|
||||
tzlocal==2.1
|
||||
urllib3==1.26.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
uvicorn[standard]==0.13.4
|
||||
uvloop==0.14.0
|
||||
watchdog==1.0.2
|
||||
watchgod==0.7
|
||||
wcwidth==0.2.5
|
||||
websockets==9.1
|
||||
whitenoise==5.3.0
|
||||
websockets==8.1
|
||||
whitenoise==5.2.0
|
||||
whoosh==2.7.4
|
||||
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Paperless scheduler
|
||||
Description=Paperless consumer
|
||||
Requires=redis.service
|
||||
|
||||
[Service]
|
||||
|
||||
+2
-2
@@ -27,8 +27,7 @@
|
||||
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
||||
"es-ES": "src/locale/messages.es_ES.xlf",
|
||||
"pl-PL": "src/locale/messages.pl_PL.xlf",
|
||||
"sv-SE": "src/locale/messages.sv_SE.xlf",
|
||||
"lb-LU": "src/locale/messages.lb_LU.xlf"
|
||||
"sv-SE": "src/locale/messages.sv_SE.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
@@ -73,6 +72,7 @@
|
||||
"optimization": true,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
|
||||
@@ -33,4 +33,4 @@ exports.config = {
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
+1
-24
@@ -1407,19 +1407,11 @@
|
||||
<context context-type="linenumber">68</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="0f72a164f5a5d10d1fbdeeb84ac80a63c04b7e1b" datatype="html">
|
||||
<source>Add item</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Used for both types and correspondents</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e" datatype="html">
|
||||
<source>Suggestions:</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
<context context-type="linenumber">29</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="27d158b47717ff9305d19866960418c603f19d55" datatype="html">
|
||||
@@ -1429,13 +1421,6 @@
|
||||
<context context-type="linenumber">3</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="21fd41eb147f55ca26362f9c398e47733639837a" datatype="html">
|
||||
<source>Add tag</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4eb84de23219c85432e38fb4fbdeb6c0f103ff8b" datatype="html">
|
||||
<source>Show all</source>
|
||||
<context-group purpose="location">
|
||||
@@ -1904,14 +1889,6 @@
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4460262093225954455" datatype="html">
|
||||
<source>Search score</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
|
||||
<context context-type="linenumber">28</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="4561076822163447092" datatype="html">
|
||||
<source>Create new item</source>
|
||||
<context-group purpose="location">
|
||||
|
||||
Generated
+4473
-20627
File diff suppressed because it is too large.
Load diff
+21
-20
@@ -11,41 +11,42 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.2.14",
|
||||
"@angular/common": "~11.2.14",
|
||||
"@angular/compiler": "~11.2.14",
|
||||
"@angular/core": "~11.2.14",
|
||||
"@angular/forms": "~11.2.14",
|
||||
"@angular/localize": "~11.2.14",
|
||||
"@angular/platform-browser": "~11.2.14",
|
||||
"@angular/platform-browser-dynamic": "~11.2.14",
|
||||
"@angular/router": "~11.2.14",
|
||||
"@ng-bootstrap/ng-bootstrap": "^9.1.2",
|
||||
"@ng-select/ng-select": "^7.0.0",
|
||||
"@angular/animations": "~10.1.5",
|
||||
"@angular/common": "~10.1.5",
|
||||
"@angular/compiler": "~10.1.5",
|
||||
"@angular/core": "~10.1.5",
|
||||
"@angular/forms": "~10.1.5",
|
||||
"@angular/localize": "~10.1.5",
|
||||
"@angular/platform-browser": "~10.1.5",
|
||||
"@angular/platform-browser-dynamic": "~10.1.5",
|
||||
"@angular/router": "~10.1.5",
|
||||
"@ng-bootstrap/ng-bootstrap": "^8.0.4",
|
||||
"@ng-select/ng-select": "^5.0.9",
|
||||
"bootstrap": "^4.5.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"ng-bootstrap": "^1.6.3",
|
||||
"ng2-pdf-viewer": "^6.3.2",
|
||||
"ngx-bootstrap": "^6.2.0",
|
||||
"ngx-color": "^6.2.0",
|
||||
"ngx-cookie-service": "^10.1.1",
|
||||
"ngx-file-drop": "^11.1.0",
|
||||
"ngx-drag-drop": "^2.0.0",
|
||||
"ngx-file-drop": "^10.0.0",
|
||||
"ngx-infinite-scroll": "^9.1.0",
|
||||
"rxjs": "~6.6.0",
|
||||
"tslib": "^2.0.0",
|
||||
"uuid": "^8.3.1",
|
||||
"zone.js": "~0.11.4"
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1102.13",
|
||||
"@angular/cli": "~11.2.14",
|
||||
"@angular/compiler-cli": "~11.2.14",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@angular-devkit/build-angular": "^0.1002.0",
|
||||
"@angular/cli": "~10.1.5",
|
||||
"@angular/compiler-cli": "~10.1.5",
|
||||
"@types/jasmine": "~3.5.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~6.3.3",
|
||||
"karma": "~5.0.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
@@ -53,6 +54,6 @@
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.1.5"
|
||||
"typescript": "~4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ import { SettingsComponent } from './components/manage/settings/settings.compone
|
||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
import {DocumentAsnComponent} from "./components/document-asn/document-asn.component";
|
||||
import { SplitMergeComponent } from './components/split-merge/split-merge.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '', redirectTo: 'dashboard', pathMatch: 'full'},
|
||||
{path: '', component: AppFrameComponent, children: [
|
||||
{path: 'dashboard', component: DashboardComponent },
|
||||
{path: 'documents', component: DocumentListComponent },
|
||||
{path: 'split_merge', component: SplitMergeComponent },
|
||||
{path: 'view/:id', component: DocumentListComponent },
|
||||
{path: 'documents/:id', component: DocumentDetailComponent },
|
||||
{path: 'asn/:id', component: DocumentAsnComponent },
|
||||
@@ -33,7 +35,7 @@ const routes: Routes = [
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -1,3 +1,3 @@
|
||||
<app-toasts></app-toasts>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet>
|
||||
@@ -18,7 +18,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor (private settings: SettingsService, private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router) {
|
||||
let anyWindow = (window as any)
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js';
|
||||
anyWindow.pdfWorkerSrc = '/assets/js/pdf.worker.min.js';
|
||||
this.settings.updateDarkModeSettings()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.consumerStatusService.connect()
|
||||
|
||||
|
||||
|
||||
this.successSubscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => {
|
||||
if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)) {
|
||||
this.toastService.show({title: $localize`Document added`, delay: 10000, content: $localize`Document ${status.filename} was added to paperless.`, actionName: $localize`Open document`, action: () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { DocumentCardSmallComponent } from './components/document-list/document-
|
||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component';
|
||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||
import { TextComponent } from './components/common/input/text/text.component';
|
||||
import { InputDebounceComponent } from './components/common/input/debounce/input-debounce.component';
|
||||
import { SelectComponent } from './components/common/input/select/select.component';
|
||||
import { CheckComponent } from './components/common/input/check/check.component';
|
||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component';
|
||||
@@ -64,6 +65,10 @@ import { ApiVersionInterceptor } from './interceptors/api-version.interceptor';
|
||||
import { ColorSliderModule } from 'ngx-color/slider';
|
||||
import { ColorComponent } from './components/common/input/color/color.component';
|
||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component';
|
||||
import { SplitMergeComponent } from './components/split-merge/split-merge.component';
|
||||
import { DndModule } from 'ngx-drag-drop';
|
||||
import { DocumentChooserComponent } from './components/common/document-chooser/document-chooser.component';
|
||||
import { PageChooserComponent } from './components/common/page-chooser/page-chooser.component';
|
||||
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
@@ -76,7 +81,6 @@ import localeRu from '@angular/common/locales/ru';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
import localeSv from '@angular/common/locales/sv';
|
||||
import localeLb from '@angular/common/locales/lb';
|
||||
|
||||
|
||||
registerLocaleData(localeFr)
|
||||
@@ -91,7 +95,6 @@ registerLocaleData(localeRu)
|
||||
registerLocaleData(localeEs)
|
||||
registerLocaleData(localePl)
|
||||
registerLocaleData(localeSv)
|
||||
registerLocaleData(localeLb)
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -121,6 +124,7 @@ registerLocaleData(localeLb)
|
||||
DocumentCardSmallComponent,
|
||||
BulkEditorComponent,
|
||||
TextComponent,
|
||||
InputDebounceComponent,
|
||||
SelectComponent,
|
||||
CheckComponent,
|
||||
SaveViewConfigDialogComponent,
|
||||
@@ -142,7 +146,10 @@ registerLocaleData(localeLb)
|
||||
CustomDatePipe,
|
||||
DateComponent,
|
||||
ColorComponent,
|
||||
DocumentAsnComponent
|
||||
DocumentAsnComponent,
|
||||
SplitMergeComponent,
|
||||
DocumentChooserComponent,
|
||||
PageChooserComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -155,7 +162,8 @@ registerLocaleData(localeLb)
|
||||
InfiniteScrollModule,
|
||||
PdfViewerModule,
|
||||
NgSelectModule,
|
||||
ColorSliderModule
|
||||
ColorSliderModule,
|
||||
DndModule
|
||||
],
|
||||
providers: [
|
||||
DatePipe,
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
</svg> <ng-container i18n>Documents</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="splitMergeToolVisible">
|
||||
<a class="nav-link" routerLink="split_merge" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors"/>
|
||||
</svg> <ng-container i18n>Split & merge</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DocumentDetailComponent } from '../document-detail/document-detail.comp
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type';
|
||||
import { SplitMergeService } from 'src/app/services/split-merge.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
@@ -27,7 +28,8 @@ export class AppFrameComponent implements OnInit {
|
||||
private searchService: SearchService,
|
||||
public savedViewService: SavedViewService,
|
||||
private list: DocumentListViewService,
|
||||
private meta: Meta
|
||||
private meta: Meta,
|
||||
private splitMergeService: SplitMergeService
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -106,6 +108,10 @@ export class AppFrameComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
get splitMergeToolVisible() {
|
||||
return this.splitMergeService.hasDocuments()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ConfirmDialogComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
buttonsEnabled = true
|
||||
|
||||
|
||||
confirmButtonEnabled = true
|
||||
seconds = 0
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title" i18n>Choose Documents</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="m-n2 row row-cols-paperless-cards">
|
||||
<app-document-card-small [simpleCard]="true" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents"></app-document-card-small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-danger" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-outline-primary" [disabled]="list.selected.size == 0" (click)="confirmClicked.emit()" i18n>Add Selected</button>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
$paperless-card-breakpoints: (
|
||||
0: 2, // xs
|
||||
768px: 3, //md
|
||||
992px: 4, //lg
|
||||
1200px: 5, //xl
|
||||
1400px: 6, // xxl
|
||||
1600px: 7,
|
||||
1800px: 8,
|
||||
2000px: 9
|
||||
);
|
||||
|
||||
.row-cols-paperless-cards {
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: 100% / $n_cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DocumentChooserComponent } from './document-chooser.component';
|
||||
|
||||
describe('DocumentChooserComponent', () => {
|
||||
let component: DocumentChooserComponent;
|
||||
let fixture: ComponentFixture<DocumentChooserComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DocumentChooserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DocumentChooserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-chooser',
|
||||
templateUrl: './document-chooser.component.html',
|
||||
styleUrls: ['./document-chooser.component.scss']
|
||||
})
|
||||
export class DocumentChooserComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
public list: DocumentListViewService
|
||||
) { }
|
||||
|
||||
@Output()
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
||||
ngOnInit(): void {
|
||||
this.list.selectNone()
|
||||
this.list.activateSavedView(null)
|
||||
this.list.reload()
|
||||
}
|
||||
|
||||
cancelClicked() {
|
||||
this.list.selectNone()
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
toggleSelected(d: PaperlessDocument, event: MouseEvent): void {
|
||||
if (!event.shiftKey) this.list.toggleSelected(d)
|
||||
else this.list.selectRangeTo(d)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -83,7 +83,7 @@ export class FilterableDropdownSelectionModel {
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getNonTemporary(id: number) {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="mr-1">
|
||||
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||
|
||||
@@ -11,7 +11,7 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
||||
constructor() { }
|
||||
|
||||
onChange = (newValue: T) => {};
|
||||
|
||||
|
||||
onTouched = () => {};
|
||||
|
||||
writeValue(newValue: any): void {
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
<input type="checkbox" class="custom-control-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
<label class="custom-control-label" [for]="inputId">{{title}}</label>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@ import { AbstractInputComponent } from '../abstract-input';
|
||||
})
|
||||
export class CheckComponent extends AbstractInputComponent<boolean> {
|
||||
|
||||
constructor() {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" [style.background-color]="value"> </span>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #popContent>
|
||||
<div style="min-width: 200px;" class="pb-3">
|
||||
<color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-template>
|
||||
|
||||
<input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||
@@ -30,4 +30,4 @@
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
<input type="text" [class]="cssClasses" [placeholder]="placeholder" [(ngModel)]="inputValue" (keyup)="keyUp($event)" />
|
||||
<span *ngIf="allowClear && inputValue?.length > 0" class="clear-wrapper" title="Clear all" (click)="clear()"><span aria-hidden="true" class="clear">×</span></span>
|
||||
@@ -0,0 +1,23 @@
|
||||
.clear-wrapper {
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
user-select: none;
|
||||
width: 17px;
|
||||
color: #999;
|
||||
|
||||
.clear {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover .clear {
|
||||
color: #D0021B;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputDebounceComponent } from './text.component';
|
||||
|
||||
describe('InputDebounceComponent', () => {
|
||||
let component: InputDebounceComponent;
|
||||
let fixture: ComponentFixture<InputDebounceComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ InputDebounceComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputDebounceComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Component, Input, Output, ElementRef, EventEmitter } from '@angular/core';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-debounce',
|
||||
templateUrl: './input-debounce.component.html',
|
||||
styleUrls: ['./input-debounce.component.scss']
|
||||
})
|
||||
export class InputDebounceComponent {
|
||||
|
||||
@Input()
|
||||
placeholder: string
|
||||
|
||||
@Input()
|
||||
delay: number = 500
|
||||
|
||||
@Input()
|
||||
classes: string
|
||||
|
||||
@Input()
|
||||
allowClear: boolean = true
|
||||
|
||||
@Input()
|
||||
set pattern(regexStr: string) {
|
||||
this._pattern = new RegExp(regexStr)
|
||||
}
|
||||
|
||||
_pattern: RegExp
|
||||
|
||||
@Output()
|
||||
value: EventEmitter<string> = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
inputValue: string;
|
||||
|
||||
constructor(private elementRef: ElementRef) {
|
||||
fromEvent(elementRef.nativeElement, 'keyup').pipe(
|
||||
map(() => this.inputValue),
|
||||
debounceTime(this.delay),
|
||||
distinctUntilChanged()
|
||||
).subscribe(input =>
|
||||
this.value.emit(input)
|
||||
);
|
||||
}
|
||||
|
||||
keyUp(event: any) {
|
||||
if (!this._pattern) return
|
||||
if (event.key.length == 1 && !this._pattern.test(event.key)) { // e.g. dont do for backspace
|
||||
// invalid character, prevent input
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
this.inputValue = this.inputValue.slice(this.inputValue.length, 1)
|
||||
}
|
||||
}
|
||||
|
||||
get cssClasses(): string {
|
||||
return 'form-control ' + this.classes
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.inputValue = ''
|
||||
this.value.emit(this.inputValue)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,4 +11,4 @@
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,8 +8,6 @@
|
||||
[clearable]="allowNull"
|
||||
[items]="items"
|
||||
[addTag]="allowCreateNew && addItemRef"
|
||||
addTagText="Add item"
|
||||
i18n-addTagText="Used for both types and correspondents"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
(change)="onChange(value)"
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
[hideSelected]="true"
|
||||
[addTag]="createTagRef"
|
||||
addTagText="Add tag"
|
||||
i18n-addTagText
|
||||
(change)="onChange(value)"
|
||||
(search)="onSearch($event)"
|
||||
(focus)="clearLastSearchTerm()"
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}: {{document.title}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pdf-viewer class="page-chooser-viewer" [class.splitting]="splitting" [src]="previewUrl" [original-size]="false" [zoom]=".25" [autoresize]="true" [show-borders]="true" [show-all]="true" [render-text-mode]="2" (page-rendered)="afterPageRendered($event)" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-danger" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="confirmClicked()" [disabled]="pages.length == 0">{{title}}</button>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
@import "src/theme";
|
||||
|
||||
.modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
::ng-deep .page-chooser-viewer {
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
|
||||
.pdfViewer {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.page:not(.disabled) {
|
||||
margin: 2px 3% 2rem !important;
|
||||
cursor: pointer;
|
||||
|
||||
.textLayer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.textLayer {
|
||||
background-color: $primary !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.splitting) {
|
||||
.pdfViewer {
|
||||
.page.selected {
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
content: "";
|
||||
background: $primary no-repeat 50%/50% 50%;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.textLayer {
|
||||
background-color: $primary !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.splitting {
|
||||
.pdfViewer {
|
||||
.page:not(:last-child).selected::after,
|
||||
.page:not(:last-child):not(.disabled):hover::after {
|
||||
background-color: $primary;
|
||||
width: 2rem;
|
||||
height: 100%;
|
||||
content: "Split Here";
|
||||
color: #abb2bf; // dark mode text color
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2rem;
|
||||
writing-mode: vertical-lr;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page.disabled {
|
||||
.canvasWrapper,
|
||||
.textLayer {
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PageChooserComponent } from './page-chooser.component';
|
||||
|
||||
describe('PageChooserComponent', () => {
|
||||
let component: PageChooserComponent;
|
||||
let fixture: ComponentFixture<PageChooserComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PageChooserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PageChooserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PaperlessDocument, PaperlessDocumentPart } from 'src/app/data/paperless-document';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-chooser',
|
||||
templateUrl: './page-chooser.component.html',
|
||||
styleUrls: ['./page-chooser.component.scss']
|
||||
})
|
||||
export class PageChooserComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
) { }
|
||||
|
||||
get previewUrl(): string {
|
||||
return this.documentService.getPreviewUrl(this.document.id)
|
||||
}
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@Input()
|
||||
splitting: boolean = false
|
||||
|
||||
public pages: number[]
|
||||
|
||||
public enabledPages: number[]
|
||||
|
||||
private nPages: number
|
||||
|
||||
@Output()
|
||||
public confirmPages = new EventEmitter<number[]>()
|
||||
|
||||
get title(): string {
|
||||
return this.splitting ? 'Select Split' : 'Select Pages'
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pages = (this.document as PaperlessDocumentPart).pages
|
||||
if (this.splitting && this.pages?.length) this.enabledPages = this.pages.concat([])
|
||||
if (!this.pages) this.pages = []
|
||||
}
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
confirmClicked() {
|
||||
if (this.splitting) {
|
||||
// we need pages as the full range since we wont know length of document later
|
||||
const startPage = this.pages.pop()
|
||||
for (let page = startPage + 1; page <= this.nPages; page++) {
|
||||
if (!this.enabledPages || (this.enabledPages && this.enabledPages.indexOf(page) !== -1)) this.pages.push(page)
|
||||
}
|
||||
}
|
||||
this.confirmPages.emit(this.pages.sort((a, b) => { return a - b }))
|
||||
}
|
||||
|
||||
pdfLoaded(pdf: PDFDocumentProxy) {
|
||||
this.nPages = pdf.numPages
|
||||
}
|
||||
|
||||
public afterPageRendered(pageRenderedEvent: any) {
|
||||
const pageView = pageRenderedEvent.source /* as PDFPageView */
|
||||
const div = pageView.div as HTMLDivElement
|
||||
if (this.splitting && this.enabledPages?.length) {
|
||||
div.classList.toggle('disabled', this.enabledPages.indexOf(parseInt(div.dataset.pageNumber)) == -1)
|
||||
} else {
|
||||
div.classList.toggle('selected', this.pages.indexOf(parseInt(div.dataset.pageNumber)) !== -1)
|
||||
}
|
||||
div.onclick = () => {
|
||||
let pageNum: number = parseInt(div.dataset.pageNumber)
|
||||
if (this.splitting) { // only select 1 page
|
||||
if (pageNum == this.nPages || (this.enabledPages?.length && pageNum == Math.max(...this.enabledPages))) return
|
||||
this.pages = [pageNum]
|
||||
div.parentNode.childNodes.forEach(pageEl => {
|
||||
(pageEl as Element).classList.toggle('selected', false)
|
||||
})
|
||||
div.classList.toggle('selected', true)
|
||||
} else {
|
||||
this.pages.indexOf(pageNum) !== -1 ? this.pages.splice(this.pages.indexOf(pageNum), 1) : this.pages.push(pageNum)
|
||||
div.classList.toggle('selected', this.pages.indexOf(pageNum) !== -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,4 @@
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)" i18n>Select</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,2 +1,2 @@
|
||||
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
|
||||
<a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
|
||||
<a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
|
||||
+1
-1
@@ -23,7 +23,7 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
||||
statistics: Statistics = {}
|
||||
|
||||
subscription: Subscription
|
||||
|
||||
|
||||
private getStatistics(): Observable<Statistics> {
|
||||
return this.http.get(`${environment.apiBaseUrl}statistics/`)
|
||||
}
|
||||
|
||||
-5
@@ -4,11 +4,6 @@ form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alerts-hidden {
|
||||
.btn {
|
||||
line-height: 1;
|
||||
|
||||
+1
-1
@@ -13,4 +13,4 @@
|
||||
<p i18n>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
|
||||
</ng-container>
|
||||
|
||||
</app-widget-frame>
|
||||
</app-widget-frame>
|
||||
+2
-2
@@ -4,9 +4,9 @@
|
||||
<h5 class="card-title mb-0">{{title}}</h5>
|
||||
<ng-content select ="[header-buttons]"></ng-content>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="card-body text-dark">
|
||||
<ng-content select ="[content]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,6 +38,12 @@
|
||||
</svg> <span class="d-none d-lg-inline" i18n>More like this</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="splitMerge()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors" />
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Add to split/merge</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||
|
||||
@@ -21,6 +21,7 @@ import { TextComponent } from '../common/input/text/text.component';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions';
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
|
||||
import { SplitMergeService } from 'src/app/services/split-merge.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-detail',
|
||||
@@ -76,7 +77,8 @@ export class DocumentDetailComponent implements OnInit {
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private documentTitlePipe: DocumentTitlePipe,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService) { }
|
||||
private settings: SettingsService,
|
||||
private splitMergeService: SplitMergeService) { }
|
||||
|
||||
get useNativePdfViewer(): boolean {
|
||||
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
|
||||
@@ -225,6 +227,11 @@ export class DocumentDetailComponent implements OnInit {
|
||||
this.documentListViewService.quickFilter([{rule_type: FILTER_FULLTEXT_MORELIKE, value: this.documentId.toString()}])
|
||||
}
|
||||
|
||||
splitMerge() {
|
||||
this.splitMergeService.addDocument(this.document)
|
||||
this.router.navigate(["split_merge"])
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return this.documentListViewService.hasNext(this.documentId)
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,4 +20,4 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
.metadata-column {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ml-auto mb-2 mb-xl-0 d-flex">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="addToSplitMerge()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors" />
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Add to split/merge</span>
|
||||
</button>
|
||||
|
||||
<div class="btn-group btn-group-sm mr-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||
@@ -16,6 +16,8 @@ import { MatchingModel } from 'src/app/data/matching-model';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { SplitMergeService } from 'src/app/services/split-merge.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-editor',
|
||||
@@ -41,7 +43,9 @@ export class BulkEditorComponent {
|
||||
private modalService: NgbModal,
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private settings: SettingsService,
|
||||
private toastService: ToastService
|
||||
private toastService: ToastService,
|
||||
private splitMergeService: SplitMergeService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)
|
||||
@@ -214,4 +218,9 @@ export class BulkEditorComponent {
|
||||
saveAs(result, 'documents.zip');
|
||||
})
|
||||
}
|
||||
|
||||
addToSplitMerge() {
|
||||
this.splitMergeService.addDocuments(this.list.selectedDocuments)
|
||||
this.router.navigate(["split_merge"])
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -83,7 +83,7 @@
|
||||
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 pl-4 search-score">
|
||||
<div *ngIf="document.__search_hit__" class="list-group-item bg-light text-dark border-0 d-flex p-0 pl-4 search-score">
|
||||
<small class="text-muted" i18n>Score:</small>
|
||||
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -64,4 +64,4 @@
|
||||
span ::ng-deep .match {
|
||||
color: black;
|
||||
background-color: rgb(255, 211, 66);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||
<div *ngIf="!simpleCard" style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||
<div *ngFor="let t of getTagsLimited$() | async">
|
||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
|
||||
</div>
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text">
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<ng-container *ngIf="document.correspondent && !simpleCard">
|
||||
<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>
|
||||
{{document.title | documentTitle}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer pt-0 pb-2 px-2">
|
||||
<div *ngIf="!simpleCard" class="card-footer pt-0 pb-2 px-2">
|
||||
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
||||
<button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent pl-0 p-1 border-0" title="Filter by document type"
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
|
||||
+7
-4
@@ -17,12 +17,15 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
@Input()
|
||||
selected = false
|
||||
|
||||
@Output()
|
||||
toggleSelected = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@Input()
|
||||
simpleCard: boolean = false
|
||||
|
||||
@Output()
|
||||
toggleSelected = new EventEmitter()
|
||||
|
||||
@Output()
|
||||
clickTag = new EventEmitter<number>()
|
||||
|
||||
@@ -93,6 +96,6 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
this.popover.close()
|
||||
this.popover?.close()
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@ import { Component, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule';
|
||||
import { FilterRule } from 'src/app/data/filter-rule';
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS_FULLTEXT } from 'src/app/services/rest/document.service';
|
||||
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
|
||||
@@ -52,7 +52,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
getSortFields() {
|
||||
return isFullTextFilterRule(this.list.filterRules) ? DOCUMENT_SORT_FIELDS_FULLTEXT : DOCUMENT_SORT_FIELDS
|
||||
return DOCUMENT_SORT_FIELDS
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="form-inline d-flex align-items-center">
|
||||
<div class="input-group input-group-sm flex-fill w-auto">
|
||||
<div class="input-group-prepend" ngbDropdown>
|
||||
<button class="btn btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
||||
<button class="btn btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget == t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -32,6 +32,6 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
|
||||
match: new FormControl(""),
|
||||
is_insensitive: new FormControl(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export class CorrespondentListComponent extends GenericListComponent<PaperlessCo
|
||||
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
||||
private list: DocumentListViewService,
|
||||
toastService: ToastService
|
||||
) {
|
||||
) {
|
||||
super(correspondentsService,modalService,CorrespondentEditDialogComponent, toastService)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
})
|
||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||
|
||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||
super(service, activeModal, toastService)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -49,4 +49,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
@@ -52,4 +52,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
@@ -5,4 +5,4 @@
|
||||
<path d="M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/>
|
||||
</svg>
|
||||
<h1 i18n>404 Not Found</h1>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,166 @@
|
||||
<app-page-header i18n-title title="Split & Merge Editor">
|
||||
<div class="btn-group mt-2 mt-sm-0">
|
||||
<button class="btn btn-sm btn-outline-primary" i18n-title title="Add Documents" (click)="chooseDocuments()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg> <ng-container i18n>Add Documents</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group ml-2 mt-2 mt-sm-0">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
[popoverTitle]="optionsTitle" [autoClose]="'outside'" [ngbPopover]="optionsPopoverBody" popoverClass="shadow" #optionsPopover="ngbPopover">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear" />
|
||||
</svg> <ng-container i18n>Options</ng-container>
|
||||
</button>
|
||||
<ng-template #optionsTitle>
|
||||
<ng-container i18n>Options</ng-container>
|
||||
<button type="button" class="close" aria-label="Close" (click)="optionsPopover.close()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #optionsPopoverBody>
|
||||
<div class="form-group row text-dark">
|
||||
<label for="source_options" class="col-sm-3 col-form-label pt-0" i18n>Delete source documents</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="source_options-delete" [(ngModel)]="delete_source_documents">
|
||||
<label class="custom-control-label" for="source_options-delete" [class.text-danger]="delete_source_documents"><ng-container i18n>Delete</ng-container><br/><small [class.text-muted]="!delete_source_documents" i18n>Source documents will be immediately deleted.</small></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-4 row text-dark">
|
||||
<label for="metadata_options" class="col-sm-3 col-form-label pt-0" i18n>Output document metadata</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="metadata_setting-copy" name="metadata_setting" class="custom-control-input" [value]="splitMergeMetadata.COPY_FIRST" [(ngModel)]="metadata_setting">
|
||||
<label class="custom-control-label" for="metadata_setting-copy"><ng-container i18n>First document</ng-container><br/><small class="text-muted" i18n>Metadata will be copied from first document.</small></label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="metadata_setting-redo" name="metadata_setting" class="custom-control-input" [value]="splitMergeMetadata.REDO" [(ngModel)]="metadata_setting">
|
||||
<label class="custom-control-label" for="metadata_setting-redo"><ng-container i18n>Redo</ng-container><br/><small class="text-muted" i18n>Metadata detection will be re-run on new document.</small></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="btn-group ml-2 mr-2 mr-sm-5 mt-2 mt-sm-0">
|
||||
<button class="btn btn-sm btn-outline-primary" i18n-title title="Save" (click)="save()" [disabled]="splitMergeService.getDocuments().length < 1">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check-circle" />
|
||||
</svg> <ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group ml-sm-5 mt-2 mt-sm-0">
|
||||
<button class="btn btn-sm btn-outline-danger" i18n-title title="Cancel" (click)="cancel()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
||||
</svg> <ng-container i18n>Cancel</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="h-100" dndDropzone (dndDrop)="onDrop($event, documents)">
|
||||
<div class="spacer bg-primary" dndPlaceholderRef>
|
||||
</div>
|
||||
<div *ngFor="let d of documents; let i = index" class="card w-100 mb-3" [class.separator-card]="d.is_separator" [dndDraggable]="d" dndEffectAllowed="move" (dndMoved)="onDragged(d, documents)">
|
||||
<ng-container *ngIf="d.is_separator">
|
||||
<div class="separator"></div>
|
||||
<h6 class="text-muted small font-weight-bold d-inline-block py-2 px-3 mb-0 mx-auto bg-body" i18n>Document End</h6>
|
||||
<button class="btn btn-sm btn-danger" title="Remove" (click)="removeDocument(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div *ngIf="!d.is_separator" class="row no-gutters">
|
||||
<div class="col-md-3 bg-light d-flex handle" dndHandle>
|
||||
<svg width="2em" height="100%" viewBox="0 0 16 16" fill="currentColor" class="mx-auto">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grip-horizontal" />
|
||||
</svg>
|
||||
<div class="doc-thumb" [style.background-image]="'url(' + getThumbUrl(d.id) + ')'"></div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-2">{{d.title}}</h6>
|
||||
<div class="card-actions">
|
||||
<div class="btn-toolbar btn-toolbar-sm justify-content-between w-100">
|
||||
<button class="btn btn-sm btn-outline-danger flex-fill" i18n-title title="Remove" (click)="removeDocument(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg><span class="d-none d-xl-inline"> <ng-container i18n>Remove</ng-container></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary ml-3 flex-fill" i18n-title title="Split" (click)="chooseSplit(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors" />
|
||||
</svg><span class="d-none d-xl-inline"> <ng-container i18n>Split</ng-container></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm w-100 mt-3">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text"><ng-container i18n>Pages</ng-container>:</div>
|
||||
</div>
|
||||
<app-input-debounce delay="500" classes="form-control-sm" placeholder="All" ngbTooltip="e.g. 1-3,5" pattern="[0-9\,\-]" [inputValue]="formatPages(d.pages)" (value)="pagesFieldChange($event, d, i)"></app-input-debounce>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary" type="button" i18n-title title="Select Pages" (click)="choosePages(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg><span class="d-none d-xl-inline"> <ng-container i18n>Select Pages</ng-container></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-7">
|
||||
<ngb-alert *ngIf="error" type="danger" class="mt-2 mb-0" [dismissible]="false">
|
||||
<h6 class="alert-heading" i18n>An error occurred during operation</h6>
|
||||
<p class="mb-0 pb-1" i18n>Please check document settings and parts.</p>
|
||||
<p class="mb-0 pb-1 small"><ng-container i18n>Message</ng-container>: <span class="font-italic">{{error}}</span></p>
|
||||
</ngb-alert>
|
||||
<ng-container *ngIf="!error && previewUrls.length > 1">
|
||||
<ul ngbNav #previewNav="ngbNav" class="nav-tabs">
|
||||
<li *ngFor="let previewUrl of previewUrls; let j = index" [ngbNavItem]="j">
|
||||
<a class="bg-light" ngbNavLink><ng-container i18n>Document</ng-container> {{j + 1}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="preview-container px-2 tabbed text-center bg-light">
|
||||
<ng-container [ngTemplateOutlet]="previewHeader" [ngTemplateOutletContext]="{ $implicit: j }"></ng-container>
|
||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [render-text-mode]="2" [(page)]="previewCurrentPages[j]" (after-load-complete)="pdfPreviewLoaded($event, j)" (page-rendered)="pdfPageRendered($event, j)"></pdf-viewer>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="previewNav"></div>
|
||||
</ng-container>
|
||||
<div *ngIf="!error && previewUrls.length <= 1" class="preview-container px-2 text-center bg-light">
|
||||
<ng-container [ngTemplateOutlet]="previewHeader" [ngTemplateOutletContext]="{ $implicit: 0 }"></ng-container>
|
||||
<pdf-viewer *ngIf="previewUrls.length > 0" [src]="previewUrls[0]" [original-size]="false" [show-borders]="true" [show-all]="true" [render-text-mode]="2" [(page)]="previewCurrentPages[0]" (after-load-complete)="pdfPreviewLoaded($event, 0)" (page-rendered)="pdfPageRendered($event, 0)"></pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #previewHeader let-index>
|
||||
<div class="d-flex mt-2 p-2 w-100 justify-space-between">
|
||||
<h6 class="text-muted mt-1" [class.mx-auto]="previewNumPages[index] == undefined" [class.mr-auto]="previewNumPages[index] !== undefined" i18n>Preview</h6>
|
||||
<div *ngIf="previewNumPages[index] !== undefined" class="input-group input-group-sm w-auto">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages[index]" [(ngModel)]="previewCurrentPages[index]" />
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text" i18n>of {{previewNumPages[index]}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loading" class="spinner-border" role="status">
|
||||
<span class="sr-only" i18n>Loading...</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,116 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.card {
|
||||
.handle {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.doc-thumb {
|
||||
background-position: top center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 5rem;
|
||||
height: 100%;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
&.separator-card {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: 0.25rem;
|
||||
width: 100%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
height: calc(100vh - 170px);
|
||||
overflow: hidden;
|
||||
|
||||
&.tabbed {
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
margin-top: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.split-document-preview ::ng-deep .ng2-pdf-viewer-container {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
::ng-deep .document-selector {
|
||||
width: 80vw;
|
||||
min-width: 50vw;
|
||||
}
|
||||
|
||||
button span {
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
|
||||
app-input-debounce {
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
|
||||
::ng-deep .form-control {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link.bg-light {
|
||||
border-bottom-color: #f8f9fa;
|
||||
|
||||
&:not(.active) {
|
||||
background-color: #fff !important;
|
||||
border-bottom-color: #dee2e6;
|
||||
}
|
||||
}
|
||||
|
||||
.separator-card {
|
||||
.separator {
|
||||
border: 0;
|
||||
height: 20px;
|
||||
opacity: 0.5;
|
||||
background-color: transparent !important;
|
||||
background-image: url("data:image/svg+xml;utf8, <svg viewBox='0 0 200 110' xmlns='http://www.w3.org/2000/svg'><path d='M -15 110 L100 10 L215 110' fill='none' stroke='%23000000' stroke-width='1' vector-effect='non-scaling-stroke'/></svg>");
|
||||
background-position: bottom left;
|
||||
background-size: 2% auto;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
h6 {
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
margin-top: -20px !important;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .popover {
|
||||
min-width: 30rem;
|
||||
|
||||
.close {
|
||||
line-height: 0.7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SplitMergeComponent } from './split-merge.component';
|
||||
|
||||
describe('SplitMergeComponent', () => {
|
||||
let component: SplitMergeComponent;
|
||||
let fixture: ComponentFixture<SplitMergeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SplitMergeComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SplitMergeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { SplitMergeMetadata } from 'src/app/data/split-merge-request';
|
||||
import { SplitMergeService } from 'src/app/services/split-merge.service';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { PaperlessDocument, PaperlessDocumentPart } from 'src/app/data/paperless-document';
|
||||
import { DndDropEvent } from 'ngx-drag-drop';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DocumentChooserComponent } from 'src/app/components/common/document-chooser/document-chooser.component'
|
||||
import { PageChooserComponent } from 'src/app/components/common/page-chooser/page-chooser.component'
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-split-merge',
|
||||
templateUrl: './split-merge.component.html',
|
||||
styleUrls: ['./split-merge.component.scss']
|
||||
})
|
||||
export class SplitMergeComponent implements OnInit, OnDestroy {
|
||||
|
||||
public loading: boolean = false
|
||||
|
||||
public previewUrls: string[] = []
|
||||
public previewNumPages: number[] = []
|
||||
public previewCurrentPages: number[] = []
|
||||
|
||||
public delete_source_documents: boolean = false
|
||||
|
||||
public metadata_setting: SplitMergeMetadata = SplitMergeMetadata.COPY_FIRST
|
||||
|
||||
private previewDebounce$ = new Subject()
|
||||
|
||||
private previewSub: Subscription
|
||||
|
||||
public error: string
|
||||
|
||||
constructor(
|
||||
public splitMergeService: SplitMergeService,
|
||||
private documentService: DocumentService,
|
||||
private list: DocumentListViewService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.previewSub = this.previewDebounce$.pipe(
|
||||
debounceTime(400)
|
||||
).subscribe(() => {
|
||||
if (this.splitMergeService.hasDocuments()) {
|
||||
this.save(true)
|
||||
} else {
|
||||
this.previewUrls = []
|
||||
this.error = undefined
|
||||
}
|
||||
})
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.previewDebounce$.complete()
|
||||
}
|
||||
|
||||
get documents(): PaperlessDocument[] {
|
||||
return this.splitMergeService.getDocuments()
|
||||
}
|
||||
|
||||
getThumbUrl(documentId: number) {
|
||||
return this.documentService.getThumbUrl(documentId)
|
||||
}
|
||||
|
||||
get splitMergeMetadata() {
|
||||
return SplitMergeMetadata
|
||||
}
|
||||
|
||||
chooseDocuments() {
|
||||
let modal = this.modalService.open(DocumentChooserComponent, { backdrop: 'static', size: 'xl' })
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.splitMergeService.addDocuments(this.list.selectedDocuments)
|
||||
this.list.selectNone()
|
||||
modal.close()
|
||||
this.previewDebounce$.next()
|
||||
})
|
||||
}
|
||||
|
||||
onDragged(document: PaperlessDocument, documents: PaperlessDocument[]) {
|
||||
const index = this.documents.indexOf(document)
|
||||
this.documents.splice(index, 1)
|
||||
}
|
||||
|
||||
onDrop(event: DndDropEvent, documents: PaperlessDocument[]) {
|
||||
let index = event.index
|
||||
if (typeof index === "undefined") {
|
||||
index = this.documents.length
|
||||
}
|
||||
this.documents.splice(index, 0, event.data)
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.splitMergeService.clear()
|
||||
this.router.navigate([""])
|
||||
}
|
||||
|
||||
save(preview: boolean = false) {
|
||||
this.loading = true
|
||||
this.previewUrls = []
|
||||
this.splitMergeService.executeSplitMerge(preview, this.delete_source_documents, this.metadata_setting).subscribe(
|
||||
result => {
|
||||
this.loading = false
|
||||
this.error = undefined
|
||||
if (preview) {
|
||||
this.previewUrls = result.map(r => this.splitMergeService.getPreviewUrl(r))
|
||||
} else {
|
||||
this.splitMergeService.clear()
|
||||
this.router.navigate([""])
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.error = error
|
||||
this.loading = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
removeDocument(d: PaperlessDocument, index: number) {
|
||||
this.splitMergeService.removeDocument(d, index)
|
||||
this.previewNumPages[index] = this.previewCurrentPages[index] = undefined
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
choosePages(d: PaperlessDocument, index: number) {
|
||||
let modal = this.modalService.open(PageChooserComponent, { backdrop: 'static', size: 'lg' })
|
||||
modal.componentInstance.document = d
|
||||
modal.componentInstance.confirmPages.subscribe((pages) => {
|
||||
this.splitMergeService.setDocumentPages(d, index, pages)
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
this.previewDebounce$.next()
|
||||
})
|
||||
}
|
||||
|
||||
chooseSplit(d: PaperlessDocument, index: number) {
|
||||
let modal = this.modalService.open(PageChooserComponent, { backdrop: 'static', size: 'lg' })
|
||||
modal.componentInstance.document = d
|
||||
const enabledPages = (d as PaperlessDocumentPart).pages
|
||||
modal.componentInstance.splitting = true
|
||||
modal.componentInstance.confirmPages.subscribe((pages) => {
|
||||
this.splitMergeService.splitDocument(d, index, pages, enabledPages)
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
this.previewDebounce$.next()
|
||||
})
|
||||
}
|
||||
|
||||
pagesFieldChange(pageStr: string, d: PaperlessDocument, index: number) {
|
||||
let pages = pageStr.split(',').map(p => {
|
||||
if (p.indexOf('-') !== -1) {
|
||||
const minmax = p.split('-')
|
||||
let range = []
|
||||
for (let i = parseInt(minmax[0]); i <= parseInt(minmax[1]); i++) {
|
||||
range.push(i)
|
||||
}
|
||||
return range
|
||||
} else if (p.length > 0) {
|
||||
return parseInt(p)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
pages = [].concat.apply([], pages) // e.g. flat()
|
||||
pages = pages.filter(page => page !== null)
|
||||
this.splitMergeService.setDocumentPages(d, index, pages as number[])
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
formatPages(pages: number[]): string {
|
||||
let pageStrings = []
|
||||
let rangeStart, rangeEnd
|
||||
|
||||
pages?.forEach(page => {
|
||||
if (rangeStart == undefined) {
|
||||
rangeStart = page
|
||||
} else if (page - rangeStart == 1) {
|
||||
rangeEnd = page
|
||||
} else if (rangeEnd !== undefined && page - rangeEnd == 1) {
|
||||
rangeEnd = page
|
||||
} else {
|
||||
pageStrings.push(rangeEnd !== undefined ? rangeStart + '-' + rangeEnd : rangeStart)
|
||||
rangeStart = page
|
||||
rangeEnd = undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (rangeEnd !== undefined) {
|
||||
pageStrings.push(rangeStart + '-' + rangeEnd)
|
||||
} else if (rangeStart !== undefined) {
|
||||
pageStrings.push(rangeStart)
|
||||
}
|
||||
return pageStrings.join(',')
|
||||
}
|
||||
|
||||
pdfPreviewLoaded(pdf: PDFDocumentProxy, index: number) {
|
||||
this.previewNumPages[index] = pdf.numPages
|
||||
}
|
||||
|
||||
pdfPageRendered(event: any, index) { // CustomEvent is es6
|
||||
if (event.pageNumber > 0 && this.previewCurrentPages[index] == undefined) this.previewCurrentPages[index] = 1
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,16 @@
|
||||
import { FILTER_FULLTEXT_MORELIKE, FILTER_FULLTEXT_QUERY } from "./filter-rule-type"
|
||||
|
||||
export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
|
||||
if (filterRules) {
|
||||
let newRules: FilterRule[] = []
|
||||
for (let rule of filterRules) {
|
||||
newRules.push({rule_type: rule.rule_type, value: rule.value})
|
||||
}
|
||||
return newRules
|
||||
return newRules
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isFullTextFilterRule(filterRules: FilterRule[]): boolean {
|
||||
return filterRules.find(r => r.rule_type == FILTER_FULLTEXT_QUERY || r.rule_type == FILTER_FULLTEXT_MORELIKE) != null
|
||||
}
|
||||
|
||||
export interface FilterRule {
|
||||
rule_type: number
|
||||
value: string
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MatchingModel } from './matching-model';
|
||||
|
||||
export interface PaperlessCorrespondent extends MatchingModel {
|
||||
|
||||
|
||||
last_correspondence?: Date
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface PaperlessDocumentMetadata {
|
||||
|
||||
|
||||
original_checksum?: string
|
||||
|
||||
archived_checksum?: string
|
||||
@@ -10,4 +10,4 @@ export interface PaperlessDocumentMetadata {
|
||||
|
||||
has_archive_version?: boolean
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,4 @@ export interface PaperlessDocumentSuggestions {
|
||||
|
||||
document_types?: number[]
|
||||
|
||||
}
|
||||
}
|
||||
@@ -52,3 +52,11 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
__search_hit__?: SearchHit
|
||||
|
||||
}
|
||||
|
||||
export interface PaperlessDocumentPart extends PaperlessDocument {
|
||||
pages: number[]
|
||||
}
|
||||
|
||||
export interface PaperlessDocumentSeparator extends PaperlessDocument {
|
||||
is_separator: boolean
|
||||
}
|
||||
@@ -15,4 +15,4 @@ export interface PaperlessSavedView extends ObjectWithId {
|
||||
|
||||
filter_rules: FilterRule[]
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,5 @@ export interface Results<T> {
|
||||
count: number
|
||||
|
||||
results: T[]
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export enum SplitMergeMetadata {
|
||||
|
||||
REDO = "redo",
|
||||
|
||||
COPY_FIRST = "copy_first"
|
||||
|
||||
}
|
||||
|
||||
export interface SplitMergePlanDocumentPart {
|
||||
|
||||
document: number,
|
||||
|
||||
pages?: string
|
||||
|
||||
}
|
||||
|
||||
export type SplitMergePlan = SplitMergePlanDocumentPart[][]
|
||||
|
||||
export interface SplitMergeRequest {
|
||||
|
||||
split_merge_plan: SplitMergePlan
|
||||
|
||||
delete_source: boolean
|
||||
|
||||
metadata: SplitMergeMetadata
|
||||
|
||||
preview: boolean
|
||||
|
||||
}
|
||||
@@ -8,4 +8,4 @@ export interface WebsocketConsumerStatusMessage {
|
||||
message?: string
|
||||
document_id: number
|
||||
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export class ApiVersionInterceptor implements HttpInterceptor {
|
||||
'Accept': `application/json; version=${environment.apiVersion}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return next.handle(request);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user