mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-03 18:54:40 -05:00
Compare commits
179 Commits
feature-ma
...
v2.15.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1a72efa3c9 | ||
![]() |
aa6c58103f | ||
![]() |
ba6976bdbb | ||
![]() |
7a1be6bff2 | ||
![]() |
c3bd587cbf | ||
![]() |
a57a3dbb30 | ||
![]() |
f52ebc7bf0 | ||
![]() |
abf910fd93 | ||
![]() |
1b0aa193bd | ||
![]() |
eac12fe031 | ||
![]() |
62f46b706e | ||
![]() |
df5af5c8ad | ||
![]() |
67a97ffc4d | ||
![]() |
45b39f36d6 | ||
![]() |
a339853bc5 | ||
![]() |
6c2e06d40b | ||
![]() |
2d6a4d108f | ||
![]() |
703db0f01f | ||
![]() |
8b5d6bf3db | ||
![]() |
2d93e95848 | ||
![]() |
ab8c75958d | ||
![]() |
9db3923d35 | ||
![]() |
e2860ed36d | ||
![]() |
82a5680217 | ||
![]() |
588fd0207d | ||
![]() |
6dea158de9 | ||
![]() |
d956269d5f | ||
![]() |
f269919410 | ||
![]() |
5f00066dff | ||
![]() |
705f542129 | ||
![]() |
f036292b72 | ||
![]() |
f8a43d5dab | ||
![]() |
53c106d448 | ||
![]() |
0f37186c88 | ||
![]() |
0fb55f3ae8 | ||
![]() |
78822f6121 | ||
![]() |
2ee1d7540e | ||
![]() |
43b2527275 | ||
![]() |
248b573c03 | ||
![]() |
b9f7428f2f | ||
![]() |
e35dad81d9 | ||
![]() |
7d40eb2e24 | ||
![]() |
d2783a3fbe | ||
![]() |
8ad794e189 | ||
![]() |
358db10fe3 | ||
![]() |
556f47fe12 | ||
![]() |
0d5a2b4382 | ||
![]() |
c9bc9acd1a | ||
![]() |
e9e209d290 | ||
![]() |
2e593a0022 | ||
![]() |
3526a4cf23 | ||
![]() |
f4791cac2d | ||
![]() |
fdafd4eefb | ||
![]() |
87a8847a8d | ||
![]() |
7c31c79bbc | ||
![]() |
bf2a9b02c6 | ||
![]() |
348858780c | ||
![]() |
eb481ac1c0 | ||
![]() |
9a2d7a64ac | ||
![]() |
32a7f9cd5a | ||
![]() |
92431b2f4b | ||
![]() |
b4b2a92225 | ||
![]() |
fd45e81a83 | ||
![]() |
97d59dce9c | ||
![]() |
f3479d982c | ||
![]() |
b3ba673d9a | ||
![]() |
68b7427640 | ||
![]() |
9c68100dc0 | ||
![]() |
6e694ad9ff | ||
![]() |
5db511afdf | ||
![]() |
a8de26f88a | ||
![]() |
7a07f1e81d | ||
![]() |
92524ae97a | ||
![]() |
1c89f6da24 | ||
![]() |
d1a3e3b859 | ||
![]() |
79ae594d54 | ||
![]() |
f753f6dc46 | ||
![]() |
97fe5c4176 | ||
![]() |
1f5086164b | ||
![]() |
e60bd3a132 | ||
![]() |
b4047e73bb | ||
![]() |
4263d2196c | ||
![]() |
ac780134fb | ||
![]() |
5d6cfa7349 | ||
![]() |
3105317137 | ||
![]() |
1d9482acc3 | ||
![]() |
1456169d7f | ||
![]() |
22a6fe5e10 | ||
![]() |
caa3c13edd | ||
![]() |
24e863b298 | ||
![]() |
0c9d615f56 | ||
![]() |
4c49da9ece | ||
![]() |
a0c1a19263 | ||
![]() |
1f5d1b6f26 | ||
![]() |
3b19a727b8 | ||
![]() |
7146a5f4fc | ||
![]() |
6babc61ba2 | ||
![]() |
db5e54c6e5 | ||
![]() |
4ef5fbfb6e | ||
![]() |
b0390a92ea | ||
![]() |
70898a5064 | ||
![]() |
d07d425618 | ||
![]() |
1f60f4a636 | ||
![]() |
b29395cde7 | ||
![]() |
2f70d58219 | ||
![]() |
9944f81512 | ||
![]() |
f4413e0a08 | ||
![]() |
169aa8c8bd | ||
![]() |
94556a2607 | ||
![]() |
dcd50d5359 | ||
![]() |
376823598e | ||
![]() |
54bcbfa546 | ||
![]() |
5421e54cb0 | ||
![]() |
032bada221 | ||
![]() |
670ee6c5b0 | ||
![]() |
309fb199f2 | ||
![]() |
79328b1cec | ||
![]() |
5570d20625 | ||
![]() |
ba2cb1dec8 | ||
![]() |
4d15544a3e | ||
![]() |
955ff32dcd | ||
![]() |
b746b6f2d6 | ||
![]() |
b4b0f802e1 | ||
![]() |
5f16d5f5f1 | ||
![]() |
8db04398c7 | ||
![]() |
485dad01b7 | ||
![]() |
cf48f47a8c | ||
![]() |
f2667f5afa | ||
![]() |
d73118d226 | ||
![]() |
dd9e9a8c56 | ||
![]() |
76d363f22d | ||
![]() |
aaaa6c1393 | ||
![]() |
bed82215a0 | ||
![]() |
f8aaa5cb32 | ||
![]() |
1e489a0666 | ||
![]() |
edc7181843 | ||
![]() |
89e5c08a1f | ||
![]() |
0faa9e8865 | ||
![]() |
f205c4d0e2 | ||
![]() |
344b2bc0eb | ||
![]() |
817aad7c8b | ||
![]() |
d82555e644 | ||
![]() |
f3e6ed56b9 | ||
![]() |
780d1c67e9 | ||
![]() |
2b72397a4d | ||
![]() |
6c13ffaa01 | ||
![]() |
eb8e124971 | ||
![]() |
1bc77546eb | ||
![]() |
5a453653e2 | ||
![]() |
16f17829b6 | ||
![]() |
3cf1c04a83 | ||
![]() |
bc90ccc555 | ||
![]() |
90a332a02c | ||
![]() |
0098d1bdd5 | ||
![]() |
f6fef18a73 | ||
![]() |
6563ec6770 | ||
![]() |
755cf8619f | ||
![]() |
c6d389100c | ||
![]() |
20c4b65273 | ||
![]() |
86c94c7508 | ||
![]() |
798ece411e | ||
![]() |
654c9ca273 | ||
![]() |
628d85080f | ||
![]() |
865e9fe233 | ||
![]() |
0eb765c3e8 | ||
![]() |
ddeb741a85 | ||
![]() |
b9bcff22f8 | ||
![]() |
2d52226732 | ||
![]() |
ec34197b59 | ||
![]() |
edc0e6f859 | ||
![]() |
61cb5103ed | ||
![]() |
d364436817 | ||
![]() |
827fcba277 | ||
![]() |
90561857e8 | ||
![]() |
3104417076 | ||
![]() |
047f7c3619 | ||
![]() |
fc68f55d1a | ||
![]() |
6a8ec182fa | ||
![]() |
69541546ea |
16
.codecov.yml
16
.codecov.yml
@@ -1,18 +1,18 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
# https://docs.codecov.com/docs/flags#recommended-automatic-flag-management
|
||||
# Require each flag to have 1 upload before notification
|
||||
flag_management:
|
||||
individual_flags:
|
||||
- name: backend
|
||||
# https://docs.codecov.com/docs/components
|
||||
component_management:
|
||||
individual_components:
|
||||
- component_id: backend
|
||||
paths:
|
||||
- src/
|
||||
- name: frontend
|
||||
- src/**
|
||||
- component_id: frontend
|
||||
paths:
|
||||
- src-ui/
|
||||
- src-ui/**
|
||||
# https://docs.codecov.com/docs/pull-request-comments
|
||||
# codecov will only comment if coverage changes
|
||||
comment:
|
||||
layout: "header, diff, components, flags, files"
|
||||
require_changes: true
|
||||
# https://docs.codecov.com/docs/javascript-bundle-analysis
|
||||
require_bundle_changes: true
|
||||
|
@@ -76,18 +76,15 @@ RUN set -eux \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES}
|
||||
|
||||
ARG PYTHON_PACKAGES="\
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-wheel \
|
||||
pipenv \
|
||||
ca-certificates"
|
||||
ARG PYTHON_PACKAGES="ca-certificates"
|
||||
|
||||
RUN set -eux \
|
||||
echo "Installing python packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet ${PYTHON_PACKAGES}
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.6 /uv /bin/uv
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pre-built updates" \
|
||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
||||
@@ -131,6 +128,8 @@ RUN set -eux \
|
||||
&& echo "Configuring ImageMagick" \
|
||||
&& mv paperless-policy.xml /etc/ImageMagick-6/policy.xml
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.6 /uv /bin/uv
|
||||
|
||||
# Packages needed only for building a few quick Python
|
||||
# dependencies
|
||||
ARG BUILD_PACKAGES="\
|
||||
@@ -140,18 +139,17 @@ ARG BUILD_PACKAGES="\
|
||||
libpq-dev \
|
||||
# https://github.com/PyMySQL/mysqlclient#linux
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
pre-commit"
|
||||
pkg-config"
|
||||
|
||||
# hadolint ignore=DL3042
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
RUN --mount=type=cache,target=/root/.cache/uv,id=pip-cache \
|
||||
set -eux \
|
||||
&& echo "Installing build system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet ${BUILD_PACKAGES}
|
||||
|
||||
RUN set -eux \
|
||||
&& npm update npm -g
|
||||
&& npm update -g pnpm
|
||||
|
||||
# add users, setup scripts
|
||||
# Mount the compiled frontend to expected location
|
||||
@@ -169,9 +167,6 @@ RUN set -eux \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/.venv \
|
||||
&& echo "Adjusting all permissions" \
|
||||
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless
|
||||
# && echo "Collecting static files" \
|
||||
# && gosu paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||
# && gosu paperless python3 manage.py compilemessages
|
||||
|
||||
VOLUME ["/usr/src/paperless/paperless-ngx/data", \
|
||||
"/usr/src/paperless/paperless-ngx/media", \
|
||||
|
117
.devcontainer/README.md
Normal file
117
.devcontainer/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Paperless-ngx Development Environment
|
||||
|
||||
## Overview
|
||||
|
||||
Welcome to the Paperless-ngx development environment! This setup uses VSCode DevContainers to provide a consistent and seamless development experience.
|
||||
|
||||
### What are DevContainers?
|
||||
|
||||
DevContainers are a feature in VSCode that allows you to develop within a Docker container. This ensures that your development environment is consistent across different machines and setups. By defining a containerized environment, you can eliminate the "works on my machine" problem.
|
||||
|
||||
### Advantages of DevContainers
|
||||
|
||||
- **Consistency**: Same environment for all developers.
|
||||
- **Isolation**: Separate development environment from your local machine.
|
||||
- **Reproducibility**: Easily recreate the environment on any machine.
|
||||
- **Pre-configured Tools**: Include all necessary tools and dependencies in the container.
|
||||
|
||||
## DevContainer Setup
|
||||
|
||||
The DevContainer configuration provides up all the necessary services for Paperless-ngx, including:
|
||||
|
||||
- Redis
|
||||
- Gotenberg
|
||||
- Tika
|
||||
|
||||
Data is stored using Docker volumes to ensure persistence across container restarts.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
The setup includes debugging configurations (`launch.json`) and tasks (`tasks.json`) to help you manage and debug various parts of the project:
|
||||
|
||||
- **Backend Debugging:**
|
||||
- `manage.py runserver`
|
||||
- `manage.py document-consumer`
|
||||
- `celery`
|
||||
- **Maintenance Tasks:**
|
||||
- Create superuser
|
||||
- Run migrations
|
||||
- Recreate virtual environment (`.venv` with `uv`)
|
||||
- Compile frontend assets
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Step 1: Running the DevContainer
|
||||
|
||||
To start the DevContainer:
|
||||
|
||||
1. Open VSCode.
|
||||
2. Open the project folder.
|
||||
3. Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
4. Type and select `Dev Containers: Rebuild and Reopen in Container`.
|
||||
|
||||
VSCode will build and start the DevContainer environment.
|
||||
|
||||
### Step 2: Initial Setup
|
||||
|
||||
Once the DevContainer is up and running, perform the following steps:
|
||||
|
||||
1. **Compile Frontend Assets**:
|
||||
|
||||
- Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
- Select `Tasks: Run Task`.
|
||||
- Choose `Frontend Compile`.
|
||||
|
||||
2. **Run Database Migrations**:
|
||||
|
||||
- Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
- Select `Tasks: Run Task`.
|
||||
- Choose `Migrate Database`.
|
||||
|
||||
3. **Create Superuser**:
|
||||
- Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
- Select `Tasks: Run Task`.
|
||||
- Choose `Create Superuser`.
|
||||
|
||||
### Debugging and Running Services
|
||||
|
||||
You can start and debug backend services either as debugging sessions via `launch.json` or as tasks.
|
||||
|
||||
#### Using `launch.json`
|
||||
|
||||
1. Press `F5` or go to the **Run and Debug** view in VSCode.
|
||||
2. Select the desired configuration:
|
||||
- `Runserver`
|
||||
- `Document Consumer`
|
||||
- `Celery`
|
||||
|
||||
#### Using Tasks
|
||||
|
||||
1. Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
2. Select `Tasks: Run Task`.
|
||||
3. Choose the desired task:
|
||||
- `Runserver`
|
||||
- `Document Consumer`
|
||||
- `Celery`
|
||||
|
||||
### Additional Maintenance Tasks
|
||||
|
||||
Additional tasks are available for common maintenance operations:
|
||||
|
||||
- **Recreate .venv**: For setting up the virtual environment using `uv`.
|
||||
- **Migrate Database**: To apply database migrations.
|
||||
- **Create Superuser**: To create an admin user for the application.
|
||||
|
||||
## Let's Get Started!
|
||||
|
||||
Follow the steps above to get your development environment up and running. Happy coding!
|
@@ -3,7 +3,7 @@
|
||||
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
|
||||
"service": "paperless-development",
|
||||
"workspaceFolder": "/usr/src/paperless/paperless-ngx",
|
||||
"postCreateCommand": "pipenv install --dev && pipenv run pre-commit install",
|
||||
"postCreateCommand": "/bin/bash -c 'uv sync --group dev && uv run pre-commit install'",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
@@ -43,7 +43,7 @@ services:
|
||||
volumes:
|
||||
- ..:/usr/src/paperless/paperless-ngx:delegated
|
||||
- ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files
|
||||
- pipenv:/usr/src/paperless/paperless-ngx/.venv
|
||||
- virtualenv:/usr/src/paperless/paperless-ngx/.venv # Virtual environment persisted in volume
|
||||
- /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container
|
||||
- /usr/src/paperless/paperless-ngx/src/.pytest_cache
|
||||
- /usr/src/paperless/paperless-ngx/.ruff_cache
|
||||
@@ -80,4 +80,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pipenv:
|
||||
data:
|
||||
media:
|
||||
redisdata:
|
||||
virtualenv:
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"label": "Start: Celery Worker",
|
||||
"description": "Start the Celery Worker which processes background and consume tasks",
|
||||
"type": "shell",
|
||||
"command": "pipenv run celery --app paperless worker -l DEBUG",
|
||||
"command": "uv run celery --app paperless worker -l DEBUG",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
@@ -33,7 +33,7 @@
|
||||
"label": "Start: Frontend Angular",
|
||||
"description": "Start the Frontend Angular Dev Server",
|
||||
"type": "shell",
|
||||
"command": "npm start",
|
||||
"command": "pnpm start",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src-ui"
|
||||
@@ -61,7 +61,7 @@
|
||||
"label": "Start: Consumer Service (manage.py document_consumer)",
|
||||
"description": "Start the Consumer Service which processes files from a directory",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py document_consumer",
|
||||
"command": "uv run python manage.py document_consumer",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
@@ -80,7 +80,7 @@
|
||||
"label": "Start: Backend Server (manage.py runserver)",
|
||||
"description": "Start the Backend Server which serves the Django API and the compiled Angular frontend",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py runserver",
|
||||
"command": "uv run python manage.py runserver",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
@@ -99,7 +99,7 @@
|
||||
"label": "Maintenance: manage.py migrate",
|
||||
"description": "Apply database migrations",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py migrate",
|
||||
"command": "uv run python manage.py migrate",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
@@ -118,7 +118,7 @@
|
||||
"label": "Maintenance: Build Documentation",
|
||||
"description": "Build the documentation with MkDocs",
|
||||
"type": "shell",
|
||||
"command": "pipenv run mkdocs build --config-file mkdocs.yml && pipenv run mkdocs serve",
|
||||
"command": "uv run mkdocs build --config-file mkdocs.yml && uv run mkdocs serve",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
@@ -137,7 +137,7 @@
|
||||
"label": "Maintenance: manage.py createsuperuser",
|
||||
"description": "Create a superuser",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py createsuperuser",
|
||||
"command": "uv run python manage.py createsuperuser",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
@@ -156,7 +156,7 @@
|
||||
"label": "Maintenance: recreate .venv",
|
||||
"description": "Recreate the python virtual environment and install python dependencies",
|
||||
"type": "shell",
|
||||
"command": "rm -R -v .venv/* || pipenv install --dev",
|
||||
"command": "rm -R -v .venv/* || uv install --dev",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
@@ -173,8 +173,8 @@
|
||||
},
|
||||
{
|
||||
"label": "Maintenance: Install Frontend Dependencies",
|
||||
"description": "Install frontend (npm) dependencies",
|
||||
"type": "npm",
|
||||
"description": "Install frontend (pnpm) dependencies",
|
||||
"type": "pnpm",
|
||||
"script": "install",
|
||||
"path": "src-ui",
|
||||
"group": "clean",
|
||||
@@ -185,7 +185,7 @@
|
||||
"description": "Clean install frontend dependencies and build the frontend for production",
|
||||
"label": "Maintenance: Compile frontend for production",
|
||||
"type": "shell",
|
||||
"command": "npm ci && ./node_modules/.bin/ng build --configuration production",
|
||||
"command": "pnpm install && ./node_modules/.bin/ng build --configuration production",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
|
@@ -27,9 +27,6 @@ indent_style = space
|
||||
[*.md]
|
||||
indent_style = space
|
||||
|
||||
[Pipfile.lock]
|
||||
indent_style = space
|
||||
|
||||
# Tests don't get a line width restriction. It's still a good idea to follow
|
||||
# the 79 character rule, but in the interests of clarity, tests often need to
|
||||
# violate it.
|
||||
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [shamoon, stumpylog]
|
66
.github/dependabot.yml
vendored
66
.github/dependabot.yml
vendored
@@ -1,12 +1,15 @@
|
||||
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
# Required for uv support for now
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
|
||||
# Enable version updates for npm
|
||||
# Enable version updates for pnpm
|
||||
- package-ecosystem: "npm"
|
||||
target-branch: "dev"
|
||||
# Look for `package.json` and `lock` files in the `/src-ui` directory
|
||||
# Look for `pnpm-lock.yaml` file in the `/src-ui` directory
|
||||
directory: "/src-ui"
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
@@ -34,9 +37,8 @@ updates:
|
||||
- "eslint"
|
||||
|
||||
# Enable version updates for Python
|
||||
- package-ecosystem: "pip"
|
||||
- package-ecosystem: "uv"
|
||||
target-branch: "dev"
|
||||
# Look for a `Pipfile` in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
@@ -47,14 +49,13 @@ updates:
|
||||
# Add reviewers
|
||||
reviewers:
|
||||
- "paperless-ngx/backend"
|
||||
ignore:
|
||||
- dependency-name: "uvicorn"
|
||||
groups:
|
||||
development:
|
||||
patterns:
|
||||
- "*pytest*"
|
||||
- "ruff"
|
||||
- "mkdocs-material"
|
||||
- "pre-commit*"
|
||||
django:
|
||||
patterns:
|
||||
- "*django*"
|
||||
@@ -65,6 +66,10 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
pre-built:
|
||||
patterns:
|
||||
- psycopg*
|
||||
- zxing-cpp
|
||||
|
||||
# Enable updates for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
@@ -85,3 +90,50 @@ updates:
|
||||
- "major"
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Update Dockerfile in root directory
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- "paperless-ngx/ci-cd"
|
||||
labels:
|
||||
- "ci-cd"
|
||||
- "dependencies"
|
||||
commit-message:
|
||||
prefix: "docker"
|
||||
include: "scope"
|
||||
|
||||
# Update Docker Compose files in docker/compose directory
|
||||
- package-ecosystem: "docker-compose"
|
||||
directory: "/docker/compose/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- "paperless-ngx/ci-cd"
|
||||
labels:
|
||||
- "ci-cd"
|
||||
- "dependencies"
|
||||
commit-message:
|
||||
prefix: "docker-compose"
|
||||
include: "scope"
|
||||
groups:
|
||||
# Individual groups for each image
|
||||
gotenberg:
|
||||
patterns:
|
||||
- "docker.io/gotenberg/gotenberg*"
|
||||
tika:
|
||||
patterns:
|
||||
- "docker.io/apache/tika*"
|
||||
redis:
|
||||
patterns:
|
||||
- "docker.io/library/redis*"
|
||||
mariadb:
|
||||
patterns:
|
||||
- "docker.io/library/mariadb*"
|
||||
postgres:
|
||||
patterns:
|
||||
- "docker.io/library/postgres*"
|
||||
|
252
.github/workflows/ci.yml
vendored
252
.github/workflows/ci.yml
vendored
@@ -14,9 +14,7 @@ on:
|
||||
- 'translations**'
|
||||
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2024.4.1"
|
||||
DEFAULT_UV_VERSION: "0.6.x"
|
||||
# This is the default version of Python to use in most steps which aren't specific
|
||||
DEFAULT_PYTHON_VERSION: "3.11"
|
||||
|
||||
@@ -59,24 +57,25 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
cache-dependency-path: 'Pipfile.lock'
|
||||
-
|
||||
name: Install pipenv
|
||||
run: |
|
||||
pip install --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }}
|
||||
name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||
enable-cache: true
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
-
|
||||
name: Install dependencies
|
||||
name: Install Python dependencies
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
|
||||
-
|
||||
name: List installed Python dependencies
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pip list
|
||||
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
|
||||
-
|
||||
name: Make documentation
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs build --config-file ./mkdocs.yml
|
||||
uv run \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
--dev \
|
||||
--frozen \
|
||||
mkdocs build --config-file ./mkdocs.yml
|
||||
-
|
||||
name: Deploy documentation
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
@@ -84,7 +83,11 @@ jobs:
|
||||
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
|
||||
git config --global user.name "${{ github.actor }}"
|
||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history
|
||||
uv run \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
--dev \
|
||||
--frozen \
|
||||
mkdocs gh-deploy --force --no-history
|
||||
-
|
||||
name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -117,12 +120,13 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
cache: "pipenv"
|
||||
cache-dependency-path: 'Pipfile.lock'
|
||||
-
|
||||
name: Install pipenv
|
||||
run: |
|
||||
pip install --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }}
|
||||
name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||
enable-cache: true
|
||||
python-version: ${{ steps.setup-python.outputs.python-version }}
|
||||
-
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
@@ -135,12 +139,14 @@ jobs:
|
||||
-
|
||||
name: Install Python dependencies
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run python --version
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
|
||||
uv sync \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
--group testing \
|
||||
--frozen
|
||||
-
|
||||
name: List installed Python dependencies
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pip list
|
||||
uv pip list
|
||||
-
|
||||
name: Tests
|
||||
env:
|
||||
@@ -150,17 +156,26 @@ jobs:
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
run: |
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pytest -ra
|
||||
uv run \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
--dev \
|
||||
--frozen \
|
||||
pytest
|
||||
-
|
||||
name: Upload coverage
|
||||
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
|
||||
uses: actions/upload-artifact@v4
|
||||
name: Upload backend test results to Codecov
|
||||
if: always()
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
name: backend-coverage-report
|
||||
path: src/coverage.xml
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-python-${{ matrix.python-version }}
|
||||
files: junit.xml
|
||||
-
|
||||
name: Upload backend coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend-python-${{ matrix.python-version }}
|
||||
files: coverage.xml
|
||||
-
|
||||
name: Stop containers
|
||||
if: always()
|
||||
@@ -168,42 +183,46 @@ jobs:
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
||||
|
||||
install-frontend-depedendencies:
|
||||
install-frontend-dependencies:
|
||||
name: "Install Frontend Dependencies"
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
-
|
||||
name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
~/.pnpm-store
|
||||
~/.cache
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||
-
|
||||
name: Install dependencies
|
||||
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
|
||||
run: cd src-ui && npm ci
|
||||
run: cd src-ui && pnpm install
|
||||
-
|
||||
name: Install Playwright
|
||||
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
|
||||
run: cd src-ui && npx playwright install --with-deps
|
||||
run: cd src-ui && pnpm playwright install --with-deps
|
||||
|
||||
tests-frontend:
|
||||
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- install-frontend-depedendencies
|
||||
- install-frontend-dependencies
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -212,124 +231,88 @@ jobs:
|
||||
shard-count: [4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
-
|
||||
name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
~/.pnpm-store
|
||||
~/.cache
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||
- name: Re-link Angular cli
|
||||
run: cd src-ui && npm link @angular/cli
|
||||
run: cd src-ui && pnpm link @angular/cli
|
||||
-
|
||||
name: Linting checks
|
||||
run: cd src-ui && npm run lint
|
||||
run: cd src-ui && pnpm run lint
|
||||
-
|
||||
name: Run Jest unit tests
|
||||
run: cd src-ui && npm run test -- --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
-
|
||||
name: Upload Jest coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jest-coverage-report-${{ matrix.shard-index }}
|
||||
path: |
|
||||
src-ui/coverage/coverage-final.json
|
||||
src-ui/coverage/lcov.info
|
||||
src-ui/coverage/clover.xml
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
-
|
||||
name: Run Playwright e2e tests
|
||||
run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
-
|
||||
name: Upload Playwright test results
|
||||
name: Upload frontend test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-${{ matrix.shard-index }}
|
||||
path: src-ui/playwright-report
|
||||
retention-days: 7
|
||||
|
||||
tests-coverage-upload:
|
||||
name: "Upload to Codecov"
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- tests-backend
|
||||
- tests-frontend
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Download frontend jest coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: src-ui/coverage/
|
||||
pattern: jest-coverage-report-*
|
||||
-
|
||||
name: Download frontend playwright coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: src-ui/coverage/
|
||||
pattern: playwright-report-*
|
||||
merge-multiple: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: frontend-node-${{ matrix.node-version }}
|
||||
directory: src-ui/
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: frontend
|
||||
flags: frontend-node-${{ matrix.node-version }}
|
||||
directory: src-ui/coverage/
|
||||
# dont include backend coverage files here
|
||||
files: '!coverage.xml'
|
||||
|
||||
frontend-bundle-analysis:
|
||||
name: "Frontend Bundle Analysis"
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- tests-frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
-
|
||||
name: Download backend coverage
|
||||
uses: actions/download-artifact@v4
|
||||
name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
name: backend-coverage-report
|
||||
path: src/
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# future expansion
|
||||
flags: backend
|
||||
directory: src/
|
||||
version: 10
|
||||
-
|
||||
name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||
-
|
||||
name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
~/.pnpm-store
|
||||
~/.cache
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
|
||||
-
|
||||
name: Re-link Angular cli
|
||||
run: cd src-ui && npm link @angular/cli
|
||||
run: cd src-ui && pnpm link @angular/cli
|
||||
-
|
||||
name: Build frontend and upload analysis
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: cd src-ui && ng build --configuration=production
|
||||
run: cd src-ui && pnpm run build --configuration=production
|
||||
|
||||
build-docker-image:
|
||||
name: Build Docker image for ${{ github.ref_name }}
|
||||
@@ -472,16 +455,17 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
cache-dependency-path: 'Pipfile.lock'
|
||||
-
|
||||
name: Install pipenv + tools
|
||||
run: |
|
||||
pip install --upgrade --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} setuptools wheel
|
||||
name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||
enable-cache: true
|
||||
python-version: ${{ steps.setup-python.outputs.python-version }}
|
||||
-
|
||||
name: Install Python dependencies
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
|
||||
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
|
||||
-
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
@@ -502,17 +486,21 @@ jobs:
|
||||
-
|
||||
name: Generate requirements file
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} requirements > requirements.txt
|
||||
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
|
||||
-
|
||||
name: Compile messages
|
||||
run: |
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run python3 manage.py compilemessages
|
||||
uv run \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
manage.py compilemessages
|
||||
-
|
||||
name: Collect static files
|
||||
run: |
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run python3 manage.py collectstatic --no-input
|
||||
uv run \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
manage.py collectstatic --no-input
|
||||
-
|
||||
name: Move files
|
||||
run: |
|
||||
@@ -528,13 +516,12 @@ jobs:
|
||||
for file_name in .dockerignore \
|
||||
.env \
|
||||
Dockerfile \
|
||||
Pipfile \
|
||||
Pipfile.lock \
|
||||
pyproject.toml \
|
||||
uv.lock \
|
||||
requirements.txt \
|
||||
LICENSE \
|
||||
README.md \
|
||||
paperless.conf.example \
|
||||
gunicorn.conf.py
|
||||
paperless.conf.example
|
||||
do
|
||||
cp --verbose ${file_name} dist/paperless-ngx/
|
||||
done
|
||||
@@ -631,15 +618,17 @@ jobs:
|
||||
ref: main
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
cache-dependency-path: 'Pipfile.lock'
|
||||
-
|
||||
name: Install pipenv + tools
|
||||
run: |
|
||||
pip install --upgrade --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} setuptools wheel
|
||||
name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||
enable-cache: true
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
-
|
||||
name: Append Changelog to docs
|
||||
id: append-Changelog
|
||||
@@ -655,7 +644,10 @@ jobs:
|
||||
CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
|
||||
echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
|
||||
mv changelog-new.md changelog.md
|
||||
pipenv run pre-commit run --files changelog.md || true
|
||||
uv run \
|
||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||
--dev \
|
||||
pre-commit run --files changelog.md || true
|
||||
git config --global user.name "github-actions"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
|
||||
|
4
.github/workflows/cleanup-tags.yml
vendored
4
.github/workflows/cleanup-tags.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
-
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.10.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
-
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.10.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.pytest_cache
|
||||
junit.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@@ -32,7 +32,7 @@ repos:
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||
exclude_types:
|
||||
- pofile
|
||||
- json
|
||||
@@ -45,16 +45,19 @@ repos:
|
||||
- javascript
|
||||
- ts
|
||||
- markdown
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
additional_dependencies:
|
||||
- prettier@3.3.3
|
||||
- 'prettier-plugin-organize-imports@4.1.0'
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.6
|
||||
rev: v0.9.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "v2.5.1"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
# Dockerfile hooks
|
||||
- repo: https://github.com/AleksaC/hadolint-py
|
||||
rev: v2.12.0.3
|
||||
|
@@ -1 +0,0 @@
|
||||
3.10.15
|
87
.ruff.toml
87
.ruff.toml
@@ -1,87 +0,0 @@
|
||||
fix = true
|
||||
line-length = 88
|
||||
respect-gitignore = true
|
||||
src = ["src"]
|
||||
target-version = "py310"
|
||||
output-format = "grouped"
|
||||
show-fixes = true
|
||||
|
||||
# https://docs.astral.sh/ruff/settings/
|
||||
# https://docs.astral.sh/ruff/rules/
|
||||
[lint]
|
||||
extend-select = [
|
||||
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
||||
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||
"EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
|
||||
"ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
|
||||
"ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
|
||||
"G201", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
|
||||
"INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
|
||||
"PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
|
||||
"Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
|
||||
"RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
|
||||
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
|
||||
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
||||
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
|
||||
"TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch
|
||||
"PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
||||
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
||||
"FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
|
||||
]
|
||||
ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
||||
"src/documents/file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/management/commands/document_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/management/commands/document_exporter.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/migrations/0012_auto_20160305_0040.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/migrations/0014_document_checksum.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/migrations/1003_mime_types.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/migrations/1012_fix_archive_files.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/models.py" = ["SIM115", "PTH"] # TODO PTH Enable & remove
|
||||
"src/documents/parsers.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/signals/handlers.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tasks.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_app_config.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_classifier.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_management.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_management_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_management_exporter.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_management_thumbnails.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_migration_archive_files.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_migration_document_pages_count.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_migration_mime_type.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_sanity_check.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_tasks.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_views.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/views.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless/checks.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless/settings.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless/tests/test_checks.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless/urls.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless/views.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless_mail/mail.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless_mail/preprocessor.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless_tesseract/parsers.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001", "PTH"] # TODO PTH Enable & remove
|
||||
"src/paperless_tika/tests/test_live_tika.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/paperless_tika/tests/test_tika_parser.py" = ["PTH"] # TODO Enable & remove
|
||||
# Testing
|
||||
"*/tests/*.py" = ["E501", "SIM117"]
|
||||
# Migrations
|
||||
"*/migrations/*.py" = ["E501", "SIM", "T201"]
|
||||
# Docker specific
|
||||
"docker/rootfs/usr/local/bin/wait-for-redis.py" = ["INP001", "T201"]
|
||||
|
||||
[lint.isort]
|
||||
force-single-line = true
|
@@ -5,5 +5,6 @@
|
||||
/src-ui/ @paperless-ngx/frontend
|
||||
|
||||
/src/ @paperless-ngx/backend
|
||||
Pipfile* @paperless-ngx/backend
|
||||
pyproject.toml @paperless-ngx/backend
|
||||
uv.lock @paperless-ngx/backend
|
||||
*.py @paperless-ngx/backend
|
||||
|
@@ -81,7 +81,7 @@ Some notes about translation:
|
||||
|
||||
If a language has already been added, and you would like to contribute new translations or change existing translations, please read the "Translation" section in the README.md file for further details on that.
|
||||
|
||||
If you would like the project to be translated to another language, first head over to https://crwd.in/paperless-ngx to check if that language has already been enabled for translation.
|
||||
If you would like the project to be translated to another language, first head over to https://crowdin.com/project/paperless-ngx to check if that language has already been enabled for translation.
|
||||
If not, please request the language to be added by creating an issue on GitHub. The issue should contain:
|
||||
|
||||
- English name of the language (the localized name can be added on Crowdin).
|
||||
|
60
Dockerfile
60
Dockerfile
@@ -4,15 +4,17 @@
|
||||
# Stage: compile-frontend
|
||||
# Purpose: Compiles the frontend
|
||||
# Notes:
|
||||
# - Does NPM stuff with Typescript and such
|
||||
# - Does PNPM stuff with Typescript and such
|
||||
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim AS compile-frontend
|
||||
|
||||
COPY ./src-ui /src/src-ui
|
||||
|
||||
WORKDIR /src/src-ui
|
||||
RUN set -eux \
|
||||
&& npm update npm -g \
|
||||
&& npm ci
|
||||
&& npm update -g pnpm \
|
||||
&& npm install -g corepack@latest \
|
||||
&& corepack enable \
|
||||
&& pnpm install
|
||||
|
||||
ARG PNGX_TAG_VERSION=
|
||||
# Add the tag to the environment file if its a tagged dev build
|
||||
@@ -26,28 +28,11 @@ esac
|
||||
RUN set -eux \
|
||||
&& ./node_modules/.bin/ng build --configuration production
|
||||
|
||||
# Stage: pipenv-base
|
||||
# Purpose: Generates a requirements.txt file for building
|
||||
# Comments:
|
||||
# - pipenv dependencies are not left in the final image
|
||||
# - pipenv can't touch the final image somehow
|
||||
FROM --platform=$BUILDPLATFORM docker.io/python:3.12-alpine AS pipenv-base
|
||||
|
||||
WORKDIR /usr/src/pipenv
|
||||
|
||||
COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.4.1 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
# Stage: s6-overlay-base
|
||||
# Purpose: Installs s6-overlay and rootfs
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here either
|
||||
FROM docker.io/python:3.12-slim-bookworm AS s6-overlay-base
|
||||
FROM ghcr.io/astral-sh/uv:0.6.13-python3.12-bookworm-slim AS s6-overlay-base
|
||||
|
||||
WORKDIR /usr/src/s6
|
||||
|
||||
@@ -123,9 +108,12 @@ ARG GS_VERSION=10.03.1
|
||||
# Set Python environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# Ignore warning from Whitenoise
|
||||
# Ignore warning from Whitenoise about async iterators
|
||||
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
||||
PNGX_CONTAINERIZED=1
|
||||
PNGX_CONTAINERIZED=1 \
|
||||
# https://docs.astral.sh/uv/reference/settings/#link-mode
|
||||
UV_LINK_MODE=copy \
|
||||
UV_CACHE_DIR=/cache/uv/
|
||||
|
||||
#
|
||||
# Begin installation and configuration
|
||||
@@ -204,46 +192,29 @@ RUN set -eux \
|
||||
&& rm --force --verbose *.deb \
|
||||
&& rm --recursive --force --verbose /var/lib/apt/lists/*
|
||||
|
||||
# Copy gunicorn config
|
||||
# Changes very infrequently
|
||||
WORKDIR /usr/src/paperless/
|
||||
|
||||
COPY --chown=1000:1000 gunicorn.conf.py /usr/src/paperless/gunicorn.conf.py
|
||||
|
||||
WORKDIR /usr/src/paperless/src/
|
||||
|
||||
# Python dependencies
|
||||
# Change pretty frequently
|
||||
COPY --chown=1000:1000 --from=pipenv-base /usr/src/pipenv/requirements.txt ./
|
||||
COPY --chown=1000:1000 ["pyproject.toml", "uv.lock", "/usr/src/paperless/src/"]
|
||||
|
||||
# Packages needed only for building a few quick Python
|
||||
# dependencies
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
# https://www.psycopg.org/docs/install.html#prerequisites
|
||||
libpq-dev \
|
||||
# https://github.com/PyMySQL/mysqlclient#linux
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config"
|
||||
|
||||
ARG ZXING_VERSION=2.3.0
|
||||
ARG PSYCOPG_VERSION=3.2.4
|
||||
|
||||
# hadolint ignore=DL3042
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
RUN --mount=type=cache,target=${UV_CACHE_DIR},id=python-cache \
|
||||
set -eux \
|
||||
&& echo "Installing build system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& python3 -m pip install --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-${PSYCOPG_VERSION}/psycopg_c-${PSYCOPG_VERSION}-cp312-cp312-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-${PSYCOPG_VERSION}/psycopg_c-${PSYCOPG_VERSION}-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/zxing-${ZXING_VERSION}/zxing_cpp-${ZXING_VERSION}-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/zxing-${ZXING_VERSION}/zxing_cpp-${ZXING_VERSION}-cp312-cp312-linux_x86_64.whl \
|
||||
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
|
||||
&& uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \
|
||||
&& uv pip install --system --no-python-downloads --python-preference system --requirements requirements.txt \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||
@@ -268,6 +239,7 @@ COPY --from=compile-frontend --chown=1000:1000 /src/src/documents/static/fronten
|
||||
# add users, setup scripts
|
||||
# Mount the compiled frontend to expected location
|
||||
RUN set -eux \
|
||||
&& sed -i '1s|^#!/usr/bin/env python3|#!/command/with-contenv python3|' manage.py \
|
||||
&& echo "Setting up user/group" \
|
||||
&& addgroup --gid 1000 paperless \
|
||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||
|
102
Pipfile
102
Pipfile
@@ -1,102 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=5.1.5"
|
||||
django-allauth = {extras = ["mfa", "socialaccount"], version = "*"}
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=25.1"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
django-soft-delete = "*"
|
||||
djangorestframework = "~=3.15.2"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-spectacular = "*"
|
||||
drf-spectacular-sidecar = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
channels = "~=4.2"
|
||||
channels-redis = "*"
|
||||
concurrent-log-handler = "*"
|
||||
filelock = "*"
|
||||
flower = "*"
|
||||
gotenberg-client = "*"
|
||||
gunicorn = "*"
|
||||
httpx-oauth = "*"
|
||||
imap-tools = "*"
|
||||
inotifyrecursive = "~=0.3"
|
||||
jinja2 = "~=3.1"
|
||||
langdetect = "*"
|
||||
mysqlclient = "*"
|
||||
nltk = "*"
|
||||
ocrmypdf = "~=16.9"
|
||||
pathvalidate = "*"
|
||||
pdf2image = "*"
|
||||
psycopg = {version = "*", extras = ["c"]}
|
||||
python-dateutil = "*"
|
||||
python-dotenv = "*"
|
||||
python-gnupg = "*"
|
||||
python-ipware = "*"
|
||||
python-magic = "*"
|
||||
pyzbar = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.6"
|
||||
setproctitle = "*"
|
||||
tika-client = "*"
|
||||
tqdm = "*"
|
||||
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=6.0"
|
||||
whitenoise = "~=6.9"
|
||||
whoosh = "~=2.7"
|
||||
zxing-cpp = "*"
|
||||
|
||||
|
||||
[dev-packages]
|
||||
# Linting
|
||||
pre-commit = "*"
|
||||
ruff = "*"
|
||||
factory-boy = "*"
|
||||
# Testing
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-django = "*"
|
||||
pytest-httpx = "*"
|
||||
pytest-env = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-xdist = "*"
|
||||
pytest-mock = "*"
|
||||
pytest-rerunfailures = "*"
|
||||
imagehash = "*"
|
||||
daphne = "*"
|
||||
# Documentation
|
||||
mkdocs-material = "*"
|
||||
mkdocs-glightbox = "*"
|
||||
|
||||
[typing-dev]
|
||||
mypy = "*"
|
||||
types-Pillow = "*"
|
||||
django-filter-stubs = "*"
|
||||
types-python-dateutil = "*"
|
||||
djangorestframework-stubs = {extras= ["compatible-mypy"], version="*"}
|
||||
celery-types = "*"
|
||||
django-stubs = {extras= ["compatible-mypy"], version="*"}
|
||||
types-dateparser = "*"
|
||||
types-bleach = "*"
|
||||
types-redis = "*"
|
||||
types-tqdm = "*"
|
||||
types-Markdown = "*"
|
||||
types-Pygments = "*"
|
||||
types-colorama = "*"
|
||||
types-setuptools = "*"
|
4978
Pipfile.lock
generated
4978
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -83,7 +83,7 @@ People interested in continuing the work on paperless-ngx are encouraged to reac
|
||||
|
||||
## Translation
|
||||
|
||||
Paperless-ngx is available in many languages that are coordinated on Crowdin. If you want to help out by translating paperless-ngx into your language, please head over to https://crwd.in/paperless-ngx, and thank you! More details can be found in [CONTRIBUTING.md](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md#translating-paperless-ngx).
|
||||
Paperless-ngx is available in many languages that are coordinated on Crowdin. If you want to help out by translating paperless-ngx into your language, please head over to https://crowdin.com/project/paperless-ngx, and thank you! More details can be found in [CONTRIBUTING.md](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md#translating-paperless-ngx).
|
||||
|
||||
## Feature Requests
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:8.17
|
||||
image: docker.io/gotenberg/gotenberg:8.19
|
||||
hostname: gotenberg
|
||||
container_name: gotenberg
|
||||
network_mode: host
|
||||
|
@@ -24,8 +24,8 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
@@ -77,7 +77,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:8.17
|
||||
image: docker.io/gotenberg/gotenberg:8.19
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
|
@@ -20,7 +20,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@@ -22,10 +22,6 @@
|
||||
# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file'
|
||||
# - Modify the environment variables as needed
|
||||
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||
# - Open the list of containers, select paperless_webserver_1
|
||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||
# - Run 'python3 manage.py createsuperuser' to create a user
|
||||
# - Exit the console
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
@@ -38,7 +34,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:16
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -24,7 +24,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
@@ -38,7 +37,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:16
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
@@ -71,7 +70,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:8.17
|
||||
image: docker.io/gotenberg/gotenberg:8.19
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
|
@@ -20,7 +20,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
@@ -34,7 +33,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:16
|
||||
image: docker.io/library/postgres:17
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -24,7 +24,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
@@ -59,7 +58,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:8.17
|
||||
image: docker.io/gotenberg/gotenberg:8.19
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
|
@@ -17,7 +17,6 @@
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
|
@@ -1,179 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Source: https://github.com/sameersbn/docker-gitlab/
|
||||
map_uidgid() {
|
||||
local -r usermap_original_uid=$(id -u paperless)
|
||||
local -r usermap_original_gid=$(id -g paperless)
|
||||
local -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid}
|
||||
local -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}}
|
||||
if [[ ${usermap_new_uid} != "${usermap_original_uid}" || ${usermap_new_gid} != "${usermap_original_gid}" ]]; then
|
||||
echo "Mapping UID and GID for paperless:paperless to $usermap_new_uid:$usermap_new_gid"
|
||||
usermod --non-unique --uid "${usermap_new_uid}" paperless
|
||||
groupmod --non-unique --gid "${usermap_new_gid}" paperless
|
||||
fi
|
||||
}
|
||||
|
||||
map_folders() {
|
||||
# Export these so they can be used in docker-prepare.sh
|
||||
export DATA_DIR="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
|
||||
export MEDIA_ROOT_DIR="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
|
||||
export CONSUME_DIR="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
|
||||
}
|
||||
|
||||
custom_container_init() {
|
||||
# Mostly borrowed from the LinuxServer.io base image
|
||||
# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d
|
||||
local -r custom_script_dir="/custom-cont-init.d"
|
||||
# Tamper checking.
|
||||
# Don't run files which are owned by anyone except root
|
||||
# Don't run files which are writeable by others
|
||||
if [ -d "${custom_script_dir}" ]; then
|
||||
if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 ! -user root)" ]; then
|
||||
echo "**** Potential tampering with custom scripts detected ****"
|
||||
echo "**** The folder '${custom_script_dir}' must be owned by root ****"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 -perm -o+w)" ]; then
|
||||
echo "**** The folder '${custom_script_dir}' or some of contents have write permissions for others, which is a security risk. ****"
|
||||
echo "**** Please review the permissions and their contents to make sure they are owned by root, and can only be modified by root. ****"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Make sure custom init directory has files in it
|
||||
if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then
|
||||
echo "[custom-init] files found in ${custom_script_dir} executing"
|
||||
# Loop over files in the directory
|
||||
for SCRIPT in "${custom_script_dir}"/*; do
|
||||
NAME="$(basename "${SCRIPT}")"
|
||||
if [ -f "${SCRIPT}" ]; then
|
||||
echo "[custom-init] ${NAME}: executing..."
|
||||
/bin/bash "${SCRIPT}"
|
||||
echo "[custom-init] ${NAME}: exited $?"
|
||||
elif [ ! -f "${SCRIPT}" ]; then
|
||||
echo "[custom-init] ${NAME}: is not a file"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "[custom-init] no custom files found exiting..."
|
||||
fi
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
initialize() {
|
||||
|
||||
# Setup environment from secrets before anything else
|
||||
# Check for a version of this var with _FILE appended
|
||||
# and convert the contents to the env var value
|
||||
# Source it so export is persistent
|
||||
# shellcheck disable=SC1091
|
||||
source /sbin/env-from-file.sh
|
||||
|
||||
# Change the user and group IDs if needed
|
||||
map_uidgid
|
||||
|
||||
# Check for overrides of certain folders
|
||||
map_folders
|
||||
|
||||
local -r export_dir="/usr/src/paperless/export"
|
||||
|
||||
for dir in \
|
||||
"${export_dir}" \
|
||||
"${DATA_DIR}" "${DATA_DIR}/index" \
|
||||
"${MEDIA_ROOT_DIR}" "${MEDIA_ROOT_DIR}/documents" "${MEDIA_ROOT_DIR}/documents/originals" "${MEDIA_ROOT_DIR}/documents/thumbnails" \
|
||||
"${CONSUME_DIR}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
echo "Creating directory ${dir}"
|
||||
mkdir --parents --verbose "${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
local -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
|
||||
echo "Creating directory scratch directory ${tmp_dir}"
|
||||
mkdir --parents --verbose "${tmp_dir}"
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
chown -R paperless:paperless "${tmp_dir}"
|
||||
for dir in \
|
||||
"${export_dir}" \
|
||||
"${DATA_DIR}" \
|
||||
"${MEDIA_ROOT_DIR}" \
|
||||
"${CONSUME_DIR}"; do
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
|
||||
done
|
||||
set -e
|
||||
|
||||
"${gosu_cmd[@]}" /sbin/docker-prepare.sh
|
||||
|
||||
# Leave this last thing
|
||||
custom_container_init
|
||||
|
||||
}
|
||||
|
||||
install_languages() {
|
||||
echo "Installing languages..."
|
||||
|
||||
read -ra langs <<<"$1"
|
||||
|
||||
# Check that it is not empty
|
||||
if [ ${#langs[@]} -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Build list of packages to install
|
||||
to_install=()
|
||||
for lang in "${langs[@]}"; do
|
||||
pkg="tesseract-ocr-$lang"
|
||||
|
||||
if dpkg --status "$pkg" &>/dev/null; then
|
||||
echo "Package $pkg already installed!"
|
||||
continue
|
||||
else
|
||||
to_install+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
# Use apt only when we install packages
|
||||
if [ ${#to_install[@]} -gt 0 ]; then
|
||||
apt-get update
|
||||
|
||||
for pkg in "${to_install[@]}"; do
|
||||
|
||||
if ! apt-cache show "$pkg" &>/dev/null; then
|
||||
echo "Skipped $pkg: Package not found! :("
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Installing package $pkg..."
|
||||
if ! apt-get --assume-yes install "$pkg" &>/dev/null; then
|
||||
echo "Could not install $pkg"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Paperless-ngx docker container starting..."
|
||||
|
||||
gosu_cmd=(gosu paperless)
|
||||
if [ "$(id --user)" == "$(id --user paperless)" ]; then
|
||||
gosu_cmd=()
|
||||
fi
|
||||
|
||||
# Install additional languages if specified
|
||||
if [[ -n "$PAPERLESS_OCR_LANGUAGES" ]]; then
|
||||
install_languages "$PAPERLESS_OCR_LANGUAGES"
|
||||
fi
|
||||
|
||||
initialize
|
||||
|
||||
if [[ "$1" != "/"* ]]; then
|
||||
echo Executing management command "$@"
|
||||
exec "${gosu_cmd[@]}" python3 manage.py "$@"
|
||||
else
|
||||
echo Executing "$@"
|
||||
exec "$@"
|
||||
fi
|
@@ -18,9 +18,10 @@ for command in decrypt_documents \
|
||||
document_fuzzy_match \
|
||||
manage_superuser \
|
||||
convert_mariadb_uuid \
|
||||
prune_audit_logs;
|
||||
prune_audit_logs \
|
||||
createsuperuser;
|
||||
do
|
||||
echo "installing $command..."
|
||||
sed "s/management_command/$command/g" management_script.sh >"$PWD/rootfs/usr/local/bin/$command"
|
||||
chmod +x "$PWD/rootfs/usr/local/bin/$command"
|
||||
chmod u=rwx,g=rwx,o=rx "$PWD/rootfs/usr/local/bin/$command"
|
||||
done
|
||||
|
@@ -17,6 +17,9 @@ if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; the
|
||||
if [[ -f ${SECRETFILE} ]]; then
|
||||
# Trim off trailing _FILE
|
||||
FILESTRIP=${FILENAME//_FILE/}
|
||||
if [[ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]]; then
|
||||
echo "${log_prefix} Your secret: ${FILENAME##*/} contains a trailing newline and may not work as expected"
|
||||
fi
|
||||
# Set environment variable
|
||||
cat "${SECRETFILE}" > "${FILESTRIP}"
|
||||
echo "${log_prefix} ${FILESTRIP##*/} set from ${FILENAME##*/}"
|
||||
|
@@ -9,25 +9,57 @@ declare -r media_root_dir="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
|
||||
declare -r consume_dir="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
|
||||
declare -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
|
||||
|
||||
echo "${log_prefix} Checking for folder existence"
|
||||
declare -r main_dirs=(
|
||||
"${export_dir}"
|
||||
"${data_dir}"
|
||||
"${media_root_dir}"
|
||||
"${consume_dir}"
|
||||
"${tmp_dir}"
|
||||
)
|
||||
|
||||
for dir in \
|
||||
"${export_dir}" \
|
||||
"${data_dir}" "${data_dir}/index" \
|
||||
"${media_root_dir}" "${media_root_dir}/documents" "${media_root_dir}/documents/originals" "${media_root_dir}/documents/thumbnails" \
|
||||
"${consume_dir}" \
|
||||
"${tmp_dir}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
mkdir --parents --verbose "${dir}"
|
||||
fi
|
||||
done
|
||||
declare -r extra_dirs=(
|
||||
"${main_dirs[@]}"
|
||||
"${data_dir}/index"
|
||||
"${media_root_dir}/documents"
|
||||
"${media_root_dir}/documents/originals"
|
||||
"${media_root_dir}/documents/thumbnails"
|
||||
)
|
||||
|
||||
echo "${log_prefix} Adjusting file and folder permissions"
|
||||
for dir in \
|
||||
"${export_dir}" \
|
||||
"${data_dir}" \
|
||||
"${media_root_dir}" \
|
||||
"${consume_dir}" \
|
||||
"${tmp_dir}"; do
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
|
||||
done
|
||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||
# Non-root mode: Create directories as current user, warn about permission issues
|
||||
echo "${log_prefix} Running in non-root mode, checking directories"
|
||||
current_uid=$(id --user)
|
||||
current_gid=$(id --group)
|
||||
|
||||
for dir in "${extra_dirs[@]}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
mkdir --parents --verbose "${dir}" || echo "${log_prefix} WARNING: Could not create ${dir} - permission denied"
|
||||
fi
|
||||
# Check permissions on existing directories too
|
||||
if [[ -d "${dir}" && ! -w "${dir}" ]]; then
|
||||
echo "${log_prefix} WARNING: No write permission to ${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Warn about ownership issues
|
||||
for dir in "${main_dirs[@]}"; do
|
||||
if [[ -d "${dir}" ]]; then
|
||||
find "${dir}" -not \( -user ${current_uid} -and -group ${current_gid} \) -exec echo "${log_prefix} WARNING: Permission issue on {}: not owned by current user (${current_uid}:${current_gid})" \; 2>/dev/null || echo "${log_prefix} WARNING: Cannot check permissions on ${dir}"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Root mode: Create and fix permissions as needed
|
||||
echo "${log_prefix} Running with root privileges, adjusting directories and permissions"
|
||||
|
||||
# First create directories
|
||||
for dir in "${extra_dirs[@]}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
mkdir --parents --verbose "${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Then fix permissions on all directories
|
||||
for dir in "${main_dirs[@]}"; do
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
|
||||
done
|
||||
fi
|
||||
|
@@ -1,20 +1,18 @@
|
||||
#!/command/with-contenv /usr/bin/bash
|
||||
# shellcheck shell=bash
|
||||
declare -r log_prefix="[init-migrations]"
|
||||
|
||||
declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
|
||||
|
||||
(
|
||||
# flock is in place to prevent multiple containers from doing migrations
|
||||
# simultaneously. This also ensures that the db is ready when the command
|
||||
# of the current container starts.
|
||||
flock 200
|
||||
echo "${log_prefix} Apply database migrations..."
|
||||
cd "${PAPERLESS_SRC_DIR}"
|
||||
echo "${log_prefix} Apply database migrations..."
|
||||
|
||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||
exec python3 manage.py migrate --skip-checks --no-input
|
||||
else
|
||||
exec s6-setuidgid paperless python3 manage.py migrate --skip-checks --no-input
|
||||
fi
|
||||
cd "${PAPERLESS_SRC_DIR}"
|
||||
|
||||
) 200>"${data_dir}/migration_lock"
|
||||
# The whole migrate, with flock, needs to run as the right user
|
||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
||||
else
|
||||
exec s6-setuidgid paperless \
|
||||
s6-setlock -n "${data_dir}/migration_lock" \
|
||||
python3 manage.py migrate --skip-checks --no-input
|
||||
fi
|
||||
|
@@ -11,9 +11,10 @@ printf "/usr/src/paperless/src" > /var/run/s6/container_environment/PAPERLESS_SR
|
||||
echo $(date +%s) > /var/run/s6/container_environment/PAPERLESS_START_TIME_S
|
||||
|
||||
# Check if we're starting as a non-root user
|
||||
if [ $(id -u) == $(id -u paperless) ]; then
|
||||
if [ "$(id --user)" != "0" ]; then
|
||||
printf "true" > /var/run/s6/container_environment/USER_IS_NON_ROOT
|
||||
echo "${log_prefix} paperless-ngx docker container running under a user"
|
||||
echo "${log_prefix} paperless-ngx docker container running under a user ($(id --user):$(id --group))"
|
||||
else
|
||||
printf "/usr/src/paperless" > /var/run/s6/container_environment/HOME
|
||||
echo "${log_prefix} paperless-ngx docker container starting init as root"
|
||||
fi
|
||||
|
@@ -3,8 +3,18 @@
|
||||
|
||||
cd ${PAPERLESS_SRC_DIR}
|
||||
|
||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||
exec /usr/local/bin/gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application
|
||||
else
|
||||
exec s6-setuidgid paperless /usr/local/bin/gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application
|
||||
# Translate between things, preferring GRANIAN_
|
||||
export GRANIAN_HOST=${GRANIAN_HOST:-${PAPERLESS_BIND_ADDR:-"::"}}
|
||||
export GRANIAN_PORT=${GRANIAN_PORT:-${PAPERLESS_PORT:-8000}}
|
||||
export GRANIAN_WORKERS=${GRANIAN_WORKERS:-${PAPERLESS_WEBSERVER_WORKERS:-1}}
|
||||
|
||||
# Only set GRANIAN_URL_PATH_PREFIX if PAPERLESS_FORCE_SCRIPT_NAME is set
|
||||
if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
|
||||
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
|
||||
fi
|
||||
|
||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
||||
else
|
||||
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
||||
fi
|
||||
|
14
docker/rootfs/usr/local/bin/createsuperuser
Executable file
14
docker/rootfs/usr/local/bin/createsuperuser
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/command/with-contenv /usr/bin/bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "${PAPERLESS_SRC_DIR}"
|
||||
|
||||
if [[ $(id -u) == 0 ]]; then
|
||||
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
|
||||
elif [[ $(id -un) == "paperless" ]]; then
|
||||
python3 manage.py createsuperuser "$@"
|
||||
else
|
||||
echo "Unknown user."
|
||||
fi
|
@@ -565,19 +565,15 @@ document.
|
||||
|
||||
### Managing encryption {#encryption}
|
||||
|
||||
Documents can be stored in Paperless using GnuPG encryption.
|
||||
|
||||
!!! warning
|
||||
|
||||
Encryption is deprecated since [paperless-ng 0.9](changelog.md#paperless-ng-090) and doesn't really
|
||||
provide any additional security, since you have to store the passphrase
|
||||
in a configuration file on the same system as the encrypted documents
|
||||
for paperless to work. Furthermore, the entire text content of the
|
||||
documents is stored plain in the database, even if your documents are
|
||||
encrypted. Filenames are not encrypted as well.
|
||||
|
||||
Also, the web server provides transparent access to your encrypted
|
||||
documents.
|
||||
Encryption was removed in [paperless-ng 0.9](changelog.md#paperless-ng-090)
|
||||
because it did not really provide any additional security, the passphrase
|
||||
was stored in a configuration file on the same system as the documents.
|
||||
Furthermore, the entire text content of the documents is stored plain in
|
||||
the database, even if your documents are encrypted. Filenames are not
|
||||
encrypted as well. Finally, the web server provides transparent access to
|
||||
your encrypted documents.
|
||||
|
||||
Consider running paperless on an encrypted filesystem instead, which
|
||||
will then at least provide security against physical hardware theft.
|
||||
@@ -633,3 +629,11 @@ entries created prior to this are not removed. This command allows you to prune
|
||||
```shell
|
||||
prune_audit_logs
|
||||
```
|
||||
|
||||
### Create superuser {#create-superuser}
|
||||
|
||||
If you need to create a superuser, use the following command:
|
||||
|
||||
```shell
|
||||
createsuperuser
|
||||
```
|
||||
|
@@ -509,6 +509,12 @@ Invoice_{{ custom_fields|get_cf_value("Select Field") }}_{{ custom_fields|get_cf
|
||||
|
||||
This will create a path like `invoices/2022/01/01/Invoice_OptionTwo_20220101.pdf` if the custom field "Date Field" is set to January 1, 2022 and "Select Field" is set to `OptionTwo`.
|
||||
|
||||
You can also use a custom `slugify` filter to slufigy text:
|
||||
|
||||
```jinja
|
||||
{{ title | slugify }}
|
||||
```
|
||||
|
||||
## Automatic recovery of invalid PDFs {#pdf-recovery}
|
||||
|
||||
Paperless will attempt to "clean" certain invalid PDFs with `qpdf` before processing if, for example, the mime_type
|
||||
|
@@ -270,7 +270,7 @@ The following methods are supported:
|
||||
- `remove_tag`
|
||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||
- `modify_tags`
|
||||
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||
- `delete`
|
||||
- No `parameters` required
|
||||
- `reprocess`
|
||||
@@ -413,3 +413,8 @@ Initial API version.
|
||||
list of strings. When creating or updating a custom field value of a
|
||||
document for a select type custom field, the value should be the `id` of
|
||||
the option whereas previously was the index of the option.
|
||||
|
||||
#### Version 8
|
||||
|
||||
- The user field of document notes now returns a simplified user object
|
||||
rather than just the user ID.
|
||||
|
@@ -1,5 +1,296 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.15.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Adds better handling during folder checking/creation/permissions for non-root [@stumpylog](https://github.com/stumpylog) ([#9616](https://github.com/paperless-ngx/paperless-ngx/pull/9616))
|
||||
- Fix: Explicitly set the HOME environment to resolve issues running as root with database certificates [@stumpylog](https://github.com/stumpylog) ([#9643](https://github.com/paperless-ngx/paperless-ngx/pull/9643))
|
||||
- Fix: prevent self-linking when bulk edit doc link [@shamoon](https://github.com/shamoon) ([#9629](https://github.com/paperless-ngx/paperless-ngx/pull/9629))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore: Bump celery to 5.5.1 [@hannesortmeier](https://github.com/hannesortmeier) ([#9642](https://github.com/paperless-ngx/paperless-ngx/pull/9642))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Tweak: consistently use created date when displaying doc in list [@shamoon](https://github.com/shamoon) ([#9651](https://github.com/paperless-ngx/paperless-ngx/pull/9651))
|
||||
- Fix: Adds better handling during folder checking/creation/permissions for non-root [@stumpylog](https://github.com/stumpylog) ([#9616](https://github.com/paperless-ngx/paperless-ngx/pull/9616))
|
||||
- Fix: Explicitly set the HOME environment to resolve issues running as root with database certificates [@stumpylog](https://github.com/stumpylog) ([#9643](https://github.com/paperless-ngx/paperless-ngx/pull/9643))
|
||||
- Fix: prevent self-linking when bulk edit doc link [@shamoon](https://github.com/shamoon) ([#9629](https://github.com/paperless-ngx/paperless-ngx/pull/9629))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.15.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Run migration lock as the correct user [@stumpylog](https://github.com/stumpylog) ([#9604](https://github.com/paperless-ngx/paperless-ngx/pull/9604))
|
||||
- Fix: Adds a warning to the user if their secret file includes a trailing newline [@stumpylog](https://github.com/stumpylog) ([#9601](https://github.com/paperless-ngx/paperless-ngx/pull/9601))
|
||||
- Fix: correct download filename in 2.15.0 [@shamoon](https://github.com/shamoon) ([#9599](https://github.com/paperless-ngx/paperless-ngx/pull/9599))
|
||||
- Fix: dont exclude matching check for scheduled workflows [@shamoon](https://github.com/shamoon) ([#9594](https://github.com/paperless-ngx/paperless-ngx/pull/9594))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- docker(deps): Bump astral-sh/uv from 0.6.9-python3.12-bookworm-slim to 0.6.13-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9573](https://github.com/paperless-ngx/paperless-ngx/pull/9573))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- docker(deps): Bump astral-sh/uv from 0.6.9-python3.12-bookworm-slim to 0.6.13-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9573](https://github.com/paperless-ngx/paperless-ngx/pull/9573))
|
||||
- Chore: move to whoosh-reloaded, for now [@shamoon](https://github.com/shamoon) ([#9605](https://github.com/paperless-ngx/paperless-ngx/pull/9605))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: Run migration lock as the correct user [@stumpylog](https://github.com/stumpylog) ([#9604](https://github.com/paperless-ngx/paperless-ngx/pull/9604))
|
||||
- Fix: Adds a warning to the user if their secret file includes a trailing newline [@stumpylog](https://github.com/stumpylog) ([#9601](https://github.com/paperless-ngx/paperless-ngx/pull/9601))
|
||||
- Fix: correct download filename in 2.15.0 [@shamoon](https://github.com/shamoon) ([#9599](https://github.com/paperless-ngx/paperless-ngx/pull/9599))
|
||||
- Fix: dont exclude matching check for scheduled workflows [@shamoon](https://github.com/shamoon) ([#9594](https://github.com/paperless-ngx/paperless-ngx/pull/9594))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.15.0
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: allow webUI first account signup [@shamoon](https://github.com/shamoon) ([#9500](https://github.com/paperless-ngx/paperless-ngx/pull/9500))
|
||||
- Enhancement: support more 'not assigned' filtering, refactor [@shamoon](https://github.com/shamoon) ([#9429](https://github.com/paperless-ngx/paperless-ngx/pull/9429))
|
||||
- Enhancement: reorganize dates dropdown, add more relative options [@shamoon](https://github.com/shamoon) ([#9307](https://github.com/paperless-ngx/paperless-ngx/pull/9307))
|
||||
- Enhancement: add switch to allow merging non-PDFs with archive version [@shamoon](https://github.com/shamoon) ([#9305](https://github.com/paperless-ngx/paperless-ngx/pull/9305))
|
||||
- Enhancement: support assigning custom field values in workflows [@shamoon](https://github.com/shamoon) ([#9272](https://github.com/paperless-ngx/paperless-ngx/pull/9272))
|
||||
- Enhancement: Add slugify filter in templating [@hwaterke](https://github.com/hwaterke) ([#9269](https://github.com/paperless-ngx/paperless-ngx/pull/9269))
|
||||
- Feature: Switch webserver to granian [@stumpylog](https://github.com/stumpylog) ([#9218](https://github.com/paperless-ngx/paperless-ngx/pull/9218))
|
||||
- Enhancement: relocate and smaller upload widget, dont limit upload list [@shamoon](https://github.com/shamoon) ([#9244](https://github.com/paperless-ngx/paperless-ngx/pull/9244))
|
||||
- Enhancement: run tasks from system status, report sanity check, simpler classifier check, styling updates [@shamoon](https://github.com/shamoon) ([#9106](https://github.com/paperless-ngx/paperless-ngx/pull/9106))
|
||||
- Enhancement: include celery log in logs view [@shamoon](https://github.com/shamoon) ([#9214](https://github.com/paperless-ngx/paperless-ngx/pull/9214))
|
||||
- Enhancement: support default groups for regular and social account signup, syncing on login [@shamoon](https://github.com/shamoon) ([#9039](https://github.com/paperless-ngx/paperless-ngx/pull/9039))
|
||||
- Enhancement: allow disabling the filesystem consumer [@shamoon](https://github.com/shamoon) ([#9199](https://github.com/paperless-ngx/paperless-ngx/pull/9199))
|
||||
- Feature: email document [@shamoon](https://github.com/shamoon) ([#8950](https://github.com/paperless-ngx/paperless-ngx/pull/8950))
|
||||
- Enhancement: webui workflowtrigger source option [@shamoon](https://github.com/shamoon) ([#9170](https://github.com/paperless-ngx/paperless-ngx/pull/9170))
|
||||
- Enhancement: use charfield for webhook url, custom validation [@shamoon](https://github.com/shamoon) ([#9128](https://github.com/paperless-ngx/paperless-ngx/pull/9128))
|
||||
- Feature: Chinese Traditional translation [@LokiHung](https://github.com/LokiHung) ([#9076](https://github.com/paperless-ngx/paperless-ngx/pull/9076))
|
||||
- Enhancement: Use cached sessions for a minor performance improvement [@stumpylog](https://github.com/stumpylog) ([#9074](https://github.com/paperless-ngx/paperless-ngx/pull/9074))
|
||||
- Feature: openapi spec, full api browser [@shamoon](https://github.com/shamoon) ([#8948](https://github.com/paperless-ngx/paperless-ngx/pull/8948))
|
||||
- Enhancement: filter by file type [@shamoon](https://github.com/shamoon) ([#8946](https://github.com/paperless-ngx/paperless-ngx/pull/8946))
|
||||
- Feature: Transition Docker to use s6 overlay [@stumpylog](https://github.com/stumpylog) ([#8886](https://github.com/paperless-ngx/paperless-ngx/pull/8886))
|
||||
- Feature: better toast notifications management [@shamoon](https://github.com/shamoon) ([#8980](https://github.com/paperless-ngx/paperless-ngx/pull/8980))
|
||||
- Enhancement: date picker and date filter dropdown improvements [@shamoon](https://github.com/shamoon) ([#9033](https://github.com/paperless-ngx/paperless-ngx/pull/9033))
|
||||
- Tweak: more accurate classifier last trained time [@shamoon](https://github.com/shamoon) ([#9004](https://github.com/paperless-ngx/paperless-ngx/pull/9004))
|
||||
- Enhancement: allow setting default pdf zoom [@shamoon](https://github.com/shamoon) ([#9017](https://github.com/paperless-ngx/paperless-ngx/pull/9017))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: ensure only matched scheduled workflows are applied [@shamoon](https://github.com/shamoon) ([#9580](https://github.com/paperless-ngx/paperless-ngx/pull/9580))
|
||||
- Fix: fix large doc thumb hidden at unexpected screen sizes [@shamoon](https://github.com/shamoon) ([#9559](https://github.com/paperless-ngx/paperless-ngx/pull/9559))
|
||||
- Fix: fix potential race condition when creating new cf from doc details [@shamoon](https://github.com/shamoon) ([#9542](https://github.com/paperless-ngx/paperless-ngx/pull/9542))
|
||||
- Fix: fix doc link input [@shamoon](https://github.com/shamoon) ([#9533](https://github.com/paperless-ngx/paperless-ngx/pull/9533))
|
||||
- Fix: only overwrite existing cf values in workflow if set [@shamoon](https://github.com/shamoon) ([#9459](https://github.com/paperless-ngx/paperless-ngx/pull/9459))
|
||||
- Fix: fix auto-close when doc update no longer has permissions [@shamoon](https://github.com/shamoon) ([#9453](https://github.com/paperless-ngx/paperless-ngx/pull/9453))
|
||||
- Change: better handle permissions in patch requests [@shamoon](https://github.com/shamoon) ([#9393](https://github.com/paperless-ngx/paperless-ngx/pull/9393))
|
||||
- Fix: use correct filename with webhook [@shamoon](https://github.com/shamoon) ([#9392](https://github.com/paperless-ngx/paperless-ngx/pull/9392))
|
||||
- Change: sync OIDC groups on first login too [@shamoon](https://github.com/shamoon) ([#9387](https://github.com/paperless-ngx/paperless-ngx/pull/9387))
|
||||
- Fix: only parse custom field queries when valid [@shamoon](https://github.com/shamoon) ([#9384](https://github.com/paperless-ngx/paperless-ngx/pull/9384))
|
||||
- Fix: Allow setting of other Granian options [@stumpylog](https://github.com/stumpylog) ([#9360](https://github.com/paperless-ngx/paperless-ngx/pull/9360))
|
||||
- Fix: Always clean up INotify [@stumpylog](https://github.com/stumpylog) ([#9359](https://github.com/paperless-ngx/paperless-ngx/pull/9359))
|
||||
- Fix typo in inactive account template [@ocean90](https://github.com/ocean90) ([#9356](https://github.com/paperless-ngx/paperless-ngx/pull/9356))
|
||||
- Fix: fix notes serializing in API document response [@shamoon](https://github.com/shamoon) ([#9336](https://github.com/paperless-ngx/paperless-ngx/pull/9336))
|
||||
- Fix: correct all results with whoosh queries [@shamoon](https://github.com/shamoon) ([#9331](https://github.com/paperless-ngx/paperless-ngx/pull/9331))
|
||||
- Fix: fix typo in altered migration [@gothicVI](https://github.com/gothicVI) ([#9321](https://github.com/paperless-ngx/paperless-ngx/pull/9321))
|
||||
- Fix: add account_inactive template / url [@shamoon](https://github.com/shamoon) ([#9322](https://github.com/paperless-ngx/paperless-ngx/pull/9322))
|
||||
- Fix: Switches data to content to upload raw bytes/text content [@stumpylog](https://github.com/stumpylog) ([#9293](https://github.com/paperless-ngx/paperless-ngx/pull/9293))
|
||||
- Fix: handle null workflow body and email subject [@shamoon](https://github.com/shamoon) ([#9271](https://github.com/paperless-ngx/paperless-ngx/pull/9271))
|
||||
- Fix: cleanup saved view references on custom field deletion, auto-refresh views, show error on saved view save [@shamoon](https://github.com/shamoon) ([#9225](https://github.com/paperless-ngx/paperless-ngx/pull/9225))
|
||||
- Fix: revert thumbnail CSS workaround in favor of GPU workaround [@shamoon](https://github.com/shamoon) ([#9219](https://github.com/paperless-ngx/paperless-ngx/pull/9219))
|
||||
- Fix: correct split confirm removal [@shamoon](https://github.com/shamoon) ([#9195](https://github.com/paperless-ngx/paperless-ngx/pull/9195))
|
||||
- Fix: saved views do not return to default display fields after setting and then removing [@shamoon](https://github.com/shamoon) ([#9168](https://github.com/paperless-ngx/paperless-ngx/pull/9168))
|
||||
- Fix: correct logged number of deleted documents on trash empty [@shamoon](https://github.com/shamoon) ([#9148](https://github.com/paperless-ngx/paperless-ngx/pull/9148))
|
||||
- Fix: include account confirm email allauth URL [@shamoon](https://github.com/shamoon) ([#9147](https://github.com/paperless-ngx/paperless-ngx/pull/9147))
|
||||
- Fix: remove additional scrollbar from popup preview [@shamoon](https://github.com/shamoon) ([#9140](https://github.com/paperless-ngx/paperless-ngx/pull/9140))
|
||||
- Fix: wrap selected display fields [@shamoon](https://github.com/shamoon) ([#9139](https://github.com/paperless-ngx/paperless-ngx/pull/9139))
|
||||
- Fix: reset documents sort field if user deletes the custom field [@shamoon](https://github.com/shamoon) ([#9127](https://github.com/paperless-ngx/paperless-ngx/pull/9127))
|
||||
- Fix: limit document title length in workflows [@shamoon](https://github.com/shamoon) ([#9085](https://github.com/paperless-ngx/paperless-ngx/pull/9085))
|
||||
- Fix: include doc link input import in custom fields query dropdown [@shamoon](https://github.com/shamoon) ([#9058](https://github.com/paperless-ngx/paperless-ngx/pull/9058))
|
||||
- Fix: deselect and trigger refresh for deleted documents from bulk operations with delete originals [@shamoon](https://github.com/shamoon) ([#8996](https://github.com/paperless-ngx/paperless-ngx/pull/8996))
|
||||
- Fix: allow empty email in profile [@shamoon](https://github.com/shamoon) ([#9012](https://github.com/paperless-ngx/paperless-ngx/pull/9012))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- docker(deps): Bump astral-sh/uv from 0.6.5-python3.12-bookworm-slim to 0.6.9-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9488](https://github.com/paperless-ngx/paperless-ngx/pull/9488))
|
||||
- Chore: Enables dependabot for Dockerfile and our Compose files [@stumpylog](https://github.com/stumpylog) ([#9342](https://github.com/paperless-ngx/paperless-ngx/pull/9342))
|
||||
- Chore: ensure codecov upload gets attempted [@shamoon](https://github.com/shamoon) ([#9308](https://github.com/paperless-ngx/paperless-ngx/pull/9308))
|
||||
- Chore: Split out some items into extras [@stumpylog](https://github.com/stumpylog) ([#9297](https://github.com/paperless-ngx/paperless-ngx/pull/9297))
|
||||
- Chore: Enables Codecov test reporting for the backend [@stumpylog](https://github.com/stumpylog) ([#9295](https://github.com/paperless-ngx/paperless-ngx/pull/9295))
|
||||
- Chore: Combine Python settings files [@stumpylog](https://github.com/stumpylog) ([#9292](https://github.com/paperless-ngx/paperless-ngx/pull/9292))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>43 changes</summary>
|
||||
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9536](https://github.com/paperless-ngx/paperless-ngx/pull/9536))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9538](https://github.com/paperless-ngx/paperless-ngx/pull/9538))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.9 to 22.13.17 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9539](https://github.com/paperless-ngx/paperless-ngx/pull/9539))
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.5.3 to 14.5.4 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9537](https://github.com/paperless-ngx/paperless-ngx/pull/9537))
|
||||
- Chore(deps-dev): Bump @playwright/test from 1.50.1 to 1.51.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9540](https://github.com/paperless-ngx/paperless-ngx/pull/9540))
|
||||
- Chore(deps): Bump django from 5.1.6 to 5.1.7 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9486](https://github.com/paperless-ngx/paperless-ngx/pull/9486))
|
||||
- docker(deps): Bump astral-sh/uv from 0.6.5-python3.12-bookworm-slim to 0.6.9-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9488](https://github.com/paperless-ngx/paperless-ngx/pull/9488))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9372](https://github.com/paperless-ngx/paperless-ngx/pull/9372))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9371](https://github.com/paperless-ngx/paperless-ngx/pull/9371))
|
||||
- Chore(deps): Update ocrmypdf requirement from ~=16.9.0 to ~=16.10.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9348](https://github.com/paperless-ngx/paperless-ngx/pull/9348))
|
||||
- Chore(deps): Update drf-spectacular-sidecar requirement from ~=2025.2.1 to ~=2025.3.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9347](https://github.com/paperless-ngx/paperless-ngx/pull/9347))
|
||||
- Chore(deps): Bump the small-changes group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9345](https://github.com/paperless-ngx/paperless-ngx/pull/9345))
|
||||
- docker-compose(deps): Bump library/postgres from 16 to 17 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#9353](https://github.com/paperless-ngx/paperless-ngx/pull/9353))
|
||||
- docker(deps): Bump astral-sh/uv from 0.6.3-python3.12-bookworm-slim to 0.6.5-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#9344](https://github.com/paperless-ngx/paperless-ngx/pull/9344))
|
||||
- Chore(deps-dev): Bump the frontend-angular-dependencies group in /src-ui with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9288](https://github.com/paperless-ngx/paperless-ngx/pull/9288))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.8 to 22.13.9 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9290](https://github.com/paperless-ngx/paperless-ngx/pull/9290))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9289](https://github.com/paperless-ngx/paperless-ngx/pull/9289))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.5 to 22.13.8 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9267](https://github.com/paperless-ngx/paperless-ngx/pull/9267))
|
||||
- Chore(deps): Bump stumpylog/image-cleaner-action from 0.9.0 to 0.10.0 in the actions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9252](https://github.com/paperless-ngx/paperless-ngx/pull/9252))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9253](https://github.com/paperless-ngx/paperless-ngx/pull/9253))
|
||||
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.8.0 to 1.9.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9260](https://github.com/paperless-ngx/paperless-ngx/pull/9260))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9256](https://github.com/paperless-ngx/paperless-ngx/pull/9256))
|
||||
- Chore(deps): Bump uuid from 11.0.5 to 11.1.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9259](https://github.com/paperless-ngx/paperless-ngx/pull/9259))
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.5.1 to 14.5.3 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9255](https://github.com/paperless-ngx/paperless-ngx/pull/9255))
|
||||
- Chore(deps): Bump rxjs from 7.8.1 to 7.8.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9258](https://github.com/paperless-ngx/paperless-ngx/pull/9258))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.0 to 22.13.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9257](https://github.com/paperless-ngx/paperless-ngx/pull/9257))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 22 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9254](https://github.com/paperless-ngx/paperless-ngx/pull/9254))
|
||||
- Chore(deps): Bump django-filter from 24.3 to 25.1 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9143](https://github.com/paperless-ngx/paperless-ngx/pull/9143))
|
||||
- Chore(deps-dev): Bump mkdocs-material from 9.6.3 to 9.6.4 in the development group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9142](https://github.com/paperless-ngx/paperless-ngx/pull/9142))
|
||||
- Dependencies: Updates to jbig2enc 0.30 [@stumpylog](https://github.com/stumpylog) ([#9092](https://github.com/paperless-ngx/paperless-ngx/pull/9092))
|
||||
- Chore(deps): Bump cryptography from 44.0.0 to 44.0.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9080](https://github.com/paperless-ngx/paperless-ngx/pull/9080))
|
||||
- Chore(deps): Bump the small-changes group with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9064](https://github.com/paperless-ngx/paperless-ngx/pull/9064))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9061](https://github.com/paperless-ngx/paperless-ngx/pull/9061))
|
||||
- Chore(deps): Bump the django group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9065](https://github.com/paperless-ngx/paperless-ngx/pull/9065))
|
||||
- Chore(deps): Bump drf-spectacular-sidecar from 2024.11.1 to 2025.2.1 in the major-versions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9063](https://github.com/paperless-ngx/paperless-ngx/pull/9063))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9013](https://github.com/paperless-ngx/paperless-ngx/pull/9013))
|
||||
- Chore(deps): Bump django-soft-delete from 1.0.16 to 1.0.18 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9014](https://github.com/paperless-ngx/paperless-ngx/pull/9014))
|
||||
- Chore(deps): Bump uuid from 11.0.2 to 11.0.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8992](https://github.com/paperless-ngx/paperless-ngx/pull/8992))
|
||||
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.2.1 to 1.8.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8991](https://github.com/paperless-ngx/paperless-ngx/pull/8991))
|
||||
- Chore(deps-dev): Bump @playwright/test from 1.48.2 to 1.50.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8993](https://github.com/paperless-ngx/paperless-ngx/pull/8993))
|
||||
- Chore(deps-dev): Bump @types/node from 22.8.6 to 22.13.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8989](https://github.com/paperless-ngx/paperless-ngx/pull/8989))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8988](https://github.com/paperless-ngx/paperless-ngx/pull/8988))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 23 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8986](https://github.com/paperless-ngx/paperless-ngx/pull/8986))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>109 changes</summary>
|
||||
|
||||
- Fix: ensure only matched scheduled workflows are applied [@shamoon](https://github.com/shamoon) ([#9580](https://github.com/paperless-ngx/paperless-ngx/pull/9580))
|
||||
- Fix: fix large doc thumb hidden at unexpected screen sizes [@shamoon](https://github.com/shamoon) ([#9559](https://github.com/paperless-ngx/paperless-ngx/pull/9559))
|
||||
- Fix: fix potential race condition when creating new cf from doc details [@shamoon](https://github.com/shamoon) ([#9542](https://github.com/paperless-ngx/paperless-ngx/pull/9542))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9536](https://github.com/paperless-ngx/paperless-ngx/pull/9536))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9538](https://github.com/paperless-ngx/paperless-ngx/pull/9538))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.9 to 22.13.17 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9539](https://github.com/paperless-ngx/paperless-ngx/pull/9539))
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.5.3 to 14.5.4 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9537](https://github.com/paperless-ngx/paperless-ngx/pull/9537))
|
||||
- Chore(deps-dev): Bump @playwright/test from 1.50.1 to 1.51.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9540](https://github.com/paperless-ngx/paperless-ngx/pull/9540))
|
||||
- Fix: fix doc link input [@shamoon](https://github.com/shamoon) ([#9533](https://github.com/paperless-ngx/paperless-ngx/pull/9533))
|
||||
- Enhancement: allow webUI first account signup [@shamoon](https://github.com/shamoon) ([#9500](https://github.com/paperless-ngx/paperless-ngx/pull/9500))
|
||||
- Fix: fix cf dropdown placement on mobile [@shamoon](https://github.com/shamoon) ([#9508](https://github.com/paperless-ngx/paperless-ngx/pull/9508))
|
||||
- Chore(deps): Bump django from 5.1.6 to 5.1.7 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9486](https://github.com/paperless-ngx/paperless-ngx/pull/9486))
|
||||
- Fix: only overwrite existing cf values in workflow if set [@shamoon](https://github.com/shamoon) ([#9459](https://github.com/paperless-ngx/paperless-ngx/pull/9459))
|
||||
- Fix: fix auto-close when doc update no longer has permissions [@shamoon](https://github.com/shamoon) ([#9453](https://github.com/paperless-ngx/paperless-ngx/pull/9453))
|
||||
- Enhancement: support more 'not assigned' filtering, refactor [@shamoon](https://github.com/shamoon) ([#9429](https://github.com/paperless-ngx/paperless-ngx/pull/9429))
|
||||
- Change: better handle permissions in patch requests [@shamoon](https://github.com/shamoon) ([#9393](https://github.com/paperless-ngx/paperless-ngx/pull/9393))
|
||||
- Fix: use correct filename with webhook [@shamoon](https://github.com/shamoon) ([#9392](https://github.com/paperless-ngx/paperless-ngx/pull/9392))
|
||||
- Change: sync OIDC groups on first login too [@shamoon](https://github.com/shamoon) ([#9387](https://github.com/paperless-ngx/paperless-ngx/pull/9387))
|
||||
- Fix: only parse custom field queries when valid [@shamoon](https://github.com/shamoon) ([#9384](https://github.com/paperless-ngx/paperless-ngx/pull/9384))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9372](https://github.com/paperless-ngx/paperless-ngx/pull/9372))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9371](https://github.com/paperless-ngx/paperless-ngx/pull/9371))
|
||||
- Development: change frontend package manager to pnpm [@shamoon](https://github.com/shamoon) ([#9363](https://github.com/paperless-ngx/paperless-ngx/pull/9363))
|
||||
- Fix: Allow setting of other Granian options [@stumpylog](https://github.com/stumpylog) ([#9360](https://github.com/paperless-ngx/paperless-ngx/pull/9360))
|
||||
- Fix: Always clean up INotify [@stumpylog](https://github.com/stumpylog) ([#9359](https://github.com/paperless-ngx/paperless-ngx/pull/9359))
|
||||
- Tweak: add saved views hint to dashboard [@shamoon](https://github.com/shamoon) ([#9362](https://github.com/paperless-ngx/paperless-ngx/pull/9362))
|
||||
- Chore(deps): Update ocrmypdf requirement from ~=16.9.0 to ~=16.10.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9348](https://github.com/paperless-ngx/paperless-ngx/pull/9348))
|
||||
- Chore(deps): Update drf-spectacular-sidecar requirement from ~=2025.2.1 to ~=2025.3.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#9347](https://github.com/paperless-ngx/paperless-ngx/pull/9347))
|
||||
- Chore(deps): Bump the small-changes group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9345](https://github.com/paperless-ngx/paperless-ngx/pull/9345))
|
||||
- Ensure the directories have been overridden and created for this test [@stumpylog](https://github.com/stumpylog) ([#9354](https://github.com/paperless-ngx/paperless-ngx/pull/9354))
|
||||
- Fix typo in inactive account template [@ocean90](https://github.com/ocean90) ([#9356](https://github.com/paperless-ngx/paperless-ngx/pull/9356))
|
||||
- Fix: fix notes serializing in API document response [@shamoon](https://github.com/shamoon) ([#9336](https://github.com/paperless-ngx/paperless-ngx/pull/9336))
|
||||
- Fix: correct all results with whoosh queries [@shamoon](https://github.com/shamoon) ([#9331](https://github.com/paperless-ngx/paperless-ngx/pull/9331))
|
||||
- Fix: fix typo in altered migration [@gothicVI](https://github.com/gothicVI) ([#9321](https://github.com/paperless-ngx/paperless-ngx/pull/9321))
|
||||
- Fix: add account_inactive template / url [@shamoon](https://github.com/shamoon) ([#9322](https://github.com/paperless-ngx/paperless-ngx/pull/9322))
|
||||
- Chore: Switch from os.path to pathlib.Path [@gothicVI](https://github.com/gothicVI) ([#9060](https://github.com/paperless-ngx/paperless-ngx/pull/9060))
|
||||
- Enhancement: reorganize dates dropdown, add more relative options [@shamoon](https://github.com/shamoon) ([#9307](https://github.com/paperless-ngx/paperless-ngx/pull/9307))
|
||||
- Chore: remove popper preventOverflow fix [@shamoon](https://github.com/shamoon) ([#9306](https://github.com/paperless-ngx/paperless-ngx/pull/9306))
|
||||
- Enhancement: add switch to allow merging non-PDFs with archive version [@shamoon](https://github.com/shamoon) ([#9305](https://github.com/paperless-ngx/paperless-ngx/pull/9305))
|
||||
- Enhancement: support assigning custom field values in workflows [@shamoon](https://github.com/shamoon) ([#9272](https://github.com/paperless-ngx/paperless-ngx/pull/9272))
|
||||
- Chore: add codecov frontend test results [@shamoon](https://github.com/shamoon) ([#9296](https://github.com/paperless-ngx/paperless-ngx/pull/9296))
|
||||
- Chore: Removes undocumented FileInfo [@stumpylog](https://github.com/stumpylog) ([#9298](https://github.com/paperless-ngx/paperless-ngx/pull/9298))
|
||||
- Chore(deps-dev): Bump the frontend-angular-dependencies group in /src-ui with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9288](https://github.com/paperless-ngx/paperless-ngx/pull/9288))
|
||||
- Fix: Switches data to content to upload raw bytes/text content [@stumpylog](https://github.com/stumpylog) ([#9293](https://github.com/paperless-ngx/paperless-ngx/pull/9293))
|
||||
- Chore: Removes the unused Log model and LogFilterSet [@stumpylog](https://github.com/stumpylog) ([#9294](https://github.com/paperless-ngx/paperless-ngx/pull/9294))
|
||||
- Chore: Combine Python settings files [@stumpylog](https://github.com/stumpylog) ([#9292](https://github.com/paperless-ngx/paperless-ngx/pull/9292))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.8 to 22.13.9 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9290](https://github.com/paperless-ngx/paperless-ngx/pull/9290))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9289](https://github.com/paperless-ngx/paperless-ngx/pull/9289))
|
||||
- Chore: Switch from pipenv to uv [@stumpylog](https://github.com/stumpylog) ([#9251](https://github.com/paperless-ngx/paperless-ngx/pull/9251))
|
||||
- Enhancement: Add slugify filter in templating [@hwaterke](https://github.com/hwaterke) ([#9269](https://github.com/paperless-ngx/paperless-ngx/pull/9269))
|
||||
- Fix: handle null workflow body and email subject [@shamoon](https://github.com/shamoon) ([#9271](https://github.com/paperless-ngx/paperless-ngx/pull/9271))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.5 to 22.13.8 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9267](https://github.com/paperless-ngx/paperless-ngx/pull/9267))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9253](https://github.com/paperless-ngx/paperless-ngx/pull/9253))
|
||||
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.8.0 to 1.9.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9260](https://github.com/paperless-ngx/paperless-ngx/pull/9260))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9256](https://github.com/paperless-ngx/paperless-ngx/pull/9256))
|
||||
- Chore(deps): Bump uuid from 11.0.5 to 11.1.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9259](https://github.com/paperless-ngx/paperless-ngx/pull/9259))
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.5.1 to 14.5.3 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9255](https://github.com/paperless-ngx/paperless-ngx/pull/9255))
|
||||
- Chore(deps): Bump rxjs from 7.8.1 to 7.8.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9258](https://github.com/paperless-ngx/paperless-ngx/pull/9258))
|
||||
- Chore(deps-dev): Bump @types/node from 22.13.0 to 22.13.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#9257](https://github.com/paperless-ngx/paperless-ngx/pull/9257))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 22 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9254](https://github.com/paperless-ngx/paperless-ngx/pull/9254))
|
||||
- Feature: Switch webserver to granian [@stumpylog](https://github.com/stumpylog) ([#9218](https://github.com/paperless-ngx/paperless-ngx/pull/9218))
|
||||
- Enhancement: relocate and smaller upload widget, dont limit upload list [@shamoon](https://github.com/shamoon) ([#9244](https://github.com/paperless-ngx/paperless-ngx/pull/9244))
|
||||
- Enhancement: run tasks from system status, report sanity check, simpler classifier check, styling updates [@shamoon](https://github.com/shamoon) ([#9106](https://github.com/paperless-ngx/paperless-ngx/pull/9106))
|
||||
- Chore: Switch remote version check to HTTPx [@stumpylog](https://github.com/stumpylog) ([#9232](https://github.com/paperless-ngx/paperless-ngx/pull/9232))
|
||||
- Fix: cleanup saved view references on custom field deletion, auto-refresh views, show error on saved view save [@shamoon](https://github.com/shamoon) ([#9225](https://github.com/paperless-ngx/paperless-ngx/pull/9225))
|
||||
- Fix: revert thumbnail CSS workaround in favor of GPU workaround [@shamoon](https://github.com/shamoon) ([#9219](https://github.com/paperless-ngx/paperless-ngx/pull/9219))
|
||||
- Chore: Reduce imports for a slight memory improvement [@stumpylog](https://github.com/stumpylog) ([#9217](https://github.com/paperless-ngx/paperless-ngx/pull/9217))
|
||||
- Enhancement: include celery log in logs view [@shamoon](https://github.com/shamoon) ([#9214](https://github.com/paperless-ngx/paperless-ngx/pull/9214))
|
||||
- Enhancement: support default groups for regular and social account signup, syncing on login [@shamoon](https://github.com/shamoon) ([#9039](https://github.com/paperless-ngx/paperless-ngx/pull/9039))
|
||||
- Enhancement: allow disabling the filesystem consumer [@shamoon](https://github.com/shamoon) ([#9199](https://github.com/paperless-ngx/paperless-ngx/pull/9199))
|
||||
- Fix: correct split confirm removal [@shamoon](https://github.com/shamoon) ([#9195](https://github.com/paperless-ngx/paperless-ngx/pull/9195))
|
||||
- Feature: email document [@shamoon](https://github.com/shamoon) ([#8950](https://github.com/paperless-ngx/paperless-ngx/pull/8950))
|
||||
- Enhancement: webui workflowtrigger source option [@shamoon](https://github.com/shamoon) ([#9170](https://github.com/paperless-ngx/paperless-ngx/pull/9170))
|
||||
- Fix: saved views do not return to default display fields after setting and then removing [@shamoon](https://github.com/shamoon) ([#9168](https://github.com/paperless-ngx/paperless-ngx/pull/9168))
|
||||
- Chore(deps): Bump django-filter from 24.3 to 25.1 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9143](https://github.com/paperless-ngx/paperless-ngx/pull/9143))
|
||||
- Chore(deps-dev): Bump mkdocs-material from 9.6.3 to 9.6.4 in the development group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9142](https://github.com/paperless-ngx/paperless-ngx/pull/9142))
|
||||
- Fix: correct logged number of deleted documents on trash empty [@shamoon](https://github.com/shamoon) ([#9148](https://github.com/paperless-ngx/paperless-ngx/pull/9148))
|
||||
- Fix: include account confirm email allauth URL [@shamoon](https://github.com/shamoon) ([#9147](https://github.com/paperless-ngx/paperless-ngx/pull/9147))
|
||||
- Fix: remove additional scrollbar from popup preview [@shamoon](https://github.com/shamoon) ([#9140](https://github.com/paperless-ngx/paperless-ngx/pull/9140))
|
||||
- Fix: wrap selected display fields [@shamoon](https://github.com/shamoon) ([#9139](https://github.com/paperless-ngx/paperless-ngx/pull/9139))
|
||||
- Enhancement: use charfield for webhook url, custom validation [@shamoon](https://github.com/shamoon) ([#9128](https://github.com/paperless-ngx/paperless-ngx/pull/9128))
|
||||
- Fix: reset documents sort field if user deletes the custom field [@shamoon](https://github.com/shamoon) ([#9127](https://github.com/paperless-ngx/paperless-ngx/pull/9127))
|
||||
- Chore: more efficient select cf update handler [@shamoon](https://github.com/shamoon) ([#9099](https://github.com/paperless-ngx/paperless-ngx/pull/9099))
|
||||
- Fix: limit document title length in workflows [@shamoon](https://github.com/shamoon) ([#9085](https://github.com/paperless-ngx/paperless-ngx/pull/9085))
|
||||
- Feature: Chinese Traditional translation [@LokiHung](https://github.com/LokiHung) ([#9076](https://github.com/paperless-ngx/paperless-ngx/pull/9076))
|
||||
- Enhancement: Use cached sessions for a minor performance improvement [@stumpylog](https://github.com/stumpylog) ([#9074](https://github.com/paperless-ngx/paperless-ngx/pull/9074))
|
||||
- Chore(deps): Bump the small-changes group with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9064](https://github.com/paperless-ngx/paperless-ngx/pull/9064))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9061](https://github.com/paperless-ngx/paperless-ngx/pull/9061))
|
||||
- Chore(deps): Bump the django group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9065](https://github.com/paperless-ngx/paperless-ngx/pull/9065))
|
||||
- Chore(deps): Bump drf-spectacular-sidecar from 2024.11.1 to 2025.2.1 in the major-versions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9063](https://github.com/paperless-ngx/paperless-ngx/pull/9063))
|
||||
- Feature: openapi spec, full api browser [@shamoon](https://github.com/shamoon) ([#8948](https://github.com/paperless-ngx/paperless-ngx/pull/8948))
|
||||
- Fix: include doc link input import in custom fields query dropdown [@shamoon](https://github.com/shamoon) ([#9058](https://github.com/paperless-ngx/paperless-ngx/pull/9058))
|
||||
- Enhancement: filter by file type [@shamoon](https://github.com/shamoon) ([#8946](https://github.com/paperless-ngx/paperless-ngx/pull/8946))
|
||||
- Enhancement: add layout options for email conversion [@RazielleS](https://github.com/RazielleS) ([#8907](https://github.com/paperless-ngx/paperless-ngx/pull/8907))
|
||||
- Chore: Enable ruff FBT [@gothicVI](https://github.com/gothicVI) ([#8645](https://github.com/paperless-ngx/paperless-ngx/pull/8645))
|
||||
- Feature: better toast notifications management [@shamoon](https://github.com/shamoon) ([#8980](https://github.com/paperless-ngx/paperless-ngx/pull/8980))
|
||||
- Enhancement: date picker and date filter dropdown improvements [@shamoon](https://github.com/shamoon) ([#9033](https://github.com/paperless-ngx/paperless-ngx/pull/9033))
|
||||
- Fix: deselect and trigger refresh for deleted documents from bulk operations with delete originals [@shamoon](https://github.com/shamoon) ([#8996](https://github.com/paperless-ngx/paperless-ngx/pull/8996))
|
||||
- Tweak: improve date matching regex for dates after numbers [@XstreamGit](https://github.com/XstreamGit) ([#8964](https://github.com/paperless-ngx/paperless-ngx/pull/8964))
|
||||
- Tweak: more accurate classifier last trained time [@shamoon](https://github.com/shamoon) ([#9004](https://github.com/paperless-ngx/paperless-ngx/pull/9004))
|
||||
- Enhancement: allow setting default pdf zoom [@shamoon](https://github.com/shamoon) ([#9017](https://github.com/paperless-ngx/paperless-ngx/pull/9017))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#9013](https://github.com/paperless-ngx/paperless-ngx/pull/9013))
|
||||
- Chore(deps): Bump django-soft-delete from 1.0.16 to 1.0.18 in the django group @[dependabot[bot]](https://github.com/apps/dependabot) ([#9014](https://github.com/paperless-ngx/paperless-ngx/pull/9014))
|
||||
- Fix: allow empty email in profile [@shamoon](https://github.com/shamoon) ([#9012](https://github.com/paperless-ngx/paperless-ngx/pull/9012))
|
||||
- Chore(deps): Bump uuid from 11.0.2 to 11.0.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8992](https://github.com/paperless-ngx/paperless-ngx/pull/8992))
|
||||
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.2.1 to 1.8.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8991](https://github.com/paperless-ngx/paperless-ngx/pull/8991))
|
||||
- Chore(deps-dev): Bump @playwright/test from 1.48.2 to 1.50.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8993](https://github.com/paperless-ngx/paperless-ngx/pull/8993))
|
||||
- Chore(deps-dev): Bump @types/node from 22.8.6 to 22.13.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8989](https://github.com/paperless-ngx/paperless-ngx/pull/8989))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8988](https://github.com/paperless-ngx/paperless-ngx/pull/8988))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 23 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8986](https://github.com/paperless-ngx/paperless-ngx/pull/8986))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.7
|
||||
|
||||
### Features
|
||||
|
@@ -404,7 +404,7 @@ set this value to /paperless. No trailing slash!
|
||||
#### [`PAPERLESS_STATIC_URL=<path>`](#PAPERLESS_STATIC_URL) {#PAPERLESS_STATIC_URL}
|
||||
|
||||
: Override the STATIC_URL here. Unless you're hosting Paperless off a
|
||||
subdomain like /paperless/, you probably don't need to change this.
|
||||
specific path like /paperless/, you probably don't need to change this.
|
||||
If you do change it, be sure to include the trailing slash.
|
||||
|
||||
Defaults to "/static/".
|
||||
@@ -557,6 +557,20 @@ This is for use with self-signed certificates against local IMAP servers.
|
||||
Settings this value has security implications for the security of your email.
|
||||
Understand what it does and be sure you need to before setting.
|
||||
|
||||
### Authentication & SSO {#authentication}
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_ALLOW_SIGNUPS=<bool>`](#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS) {#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS}
|
||||
|
||||
: Allow users to signup for a new Paperless-ngx account.
|
||||
|
||||
Defaults to False
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_DEFAULT_GROUPS=<comma-separated-list>`](#PAPERLESS_ACCOUNT_DEFAULT_GROUPS) {#PAPERLESS_ACCOUNT_DEFAULT_GROUPS}
|
||||
|
||||
: A list of group names that users will be added to when they sign up for a new account. Groups listed here must already exist.
|
||||
|
||||
Defaults to None
|
||||
|
||||
#### [`PAPERLESS_SOCIALACCOUNT_PROVIDERS=<json>`](#PAPERLESS_SOCIALACCOUNT_PROVIDERS) {#PAPERLESS_SOCIALACCOUNT_PROVIDERS}
|
||||
|
||||
: This variable is used to setup login and signup via social account providers which are compatible with django-allauth.
|
||||
@@ -580,12 +594,25 @@ system. See the corresponding
|
||||
|
||||
Defaults to True
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_ALLOW_SIGNUPS=<bool>`](#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS) {#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS}
|
||||
#### [`PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS=<bool>`](#PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS) {#PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS}
|
||||
|
||||
: Allow users to signup for a new Paperless-ngx account.
|
||||
: Sync groups from the third party authentication system (e.g. OIDC) to Paperless-ngx. When enabled, users will be added or removed from groups based on their group membership in the third party authentication system. Groups must already exist in Paperless-ngx and have the same name as in the third party authentication system. Groups are updated upon logging in via the third party authentication system, see the corresponding [django-allauth documentation](https://docs.allauth.org/en/dev/socialaccount/signals.html).
|
||||
|
||||
: In order to pass groups from the authentication system you will need to update your [PAPERLESS_SOCIALACCOUNT_PROVIDERS](#PAPERLESS_SOCIALACCOUNT_PROVIDERS) setting by adding a top-level "SCOPES" setting which includes "groups", e.g.:
|
||||
|
||||
```json
|
||||
{"openid_connect":{"SCOPE": ["openid","profile","email","groups"]...
|
||||
```
|
||||
|
||||
Defaults to False
|
||||
|
||||
#### [`PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS=<comma-separated-list>`](#PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS) {#PAPERLESS_SOCIAL_ACCOUNT_DEFAULT_GROUPS}
|
||||
|
||||
: A list of group names that users who signup via social accounts will be added to upon signup. Groups listed here must already exist.
|
||||
If both the [PAPERLESS_ACCOUNT_DEFAULT_GROUPS](#PAPERLESS_ACCOUNT_DEFAULT_GROUPS) setting and this setting are used, the user will be added to both sets of groups.
|
||||
|
||||
Defaults to None
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL=<string>`](#PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL) {#PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL}
|
||||
|
||||
: The protocol used when generating URLs, e.g. login callback URLs. See the corresponding
|
||||
@@ -1511,13 +1538,23 @@ increase RAM usage.
|
||||
|
||||
Defaults to 1.
|
||||
|
||||
!!! note
|
||||
|
||||
This option may also be set with `GRANIAN_WORKERS` and
|
||||
this option may be removed in the future
|
||||
|
||||
#### [`PAPERLESS_BIND_ADDR=<ip address>`](#PAPERLESS_BIND_ADDR) {#PAPERLESS_BIND_ADDR}
|
||||
|
||||
: The IP address the webserver will listen on inside the container.
|
||||
There are special setups where you may need to configure this value
|
||||
to restrict the Ip address or interface the webserver listens on.
|
||||
|
||||
Defaults to `[::]`, meaning all interfaces, including IPv6.
|
||||
Defaults to `::`, meaning all interfaces, including IPv6.
|
||||
|
||||
!!! note
|
||||
|
||||
This option may also be set with `GRANIAN_HOST` and
|
||||
this option may be removed in the future
|
||||
|
||||
#### [`PAPERLESS_PORT=<port>`](#PAPERLESS_PORT) {#PAPERLESS_PORT}
|
||||
|
||||
@@ -1532,6 +1569,11 @@ one pod).
|
||||
|
||||
Defaults to 8000.
|
||||
|
||||
!!! note
|
||||
|
||||
This option may also be set with `GRANIAN_PORT` and
|
||||
this option may be removed in the future
|
||||
|
||||
#### [`USERMAP_UID=<uid>`](#USERMAP_UID) {#USERMAP_UID}
|
||||
|
||||
: The ID of the paperless user in the container. Set this to your
|
||||
@@ -1628,7 +1670,7 @@ started by the container.
|
||||
|
||||
## Email sending
|
||||
|
||||
Setting an SMTP server for the backend will allow you to reset your
|
||||
Setting an SMTP server for the backend will allow you to use the Email workflow action, send documents from the UI as well as reset your
|
||||
password. All of these options come from their similarly-named [Django settings](https://docs.djangoproject.com/en/4.2/ref/settings/#email-host)
|
||||
|
||||
#### [`PAPERLESS_EMAIL_HOST=<str>`](#PAPERLESS_EMAIL_HOST) {#PAPERLESS_EMAIL_HOST}
|
||||
|
@@ -60,7 +60,7 @@ first-time setup.
|
||||
|
||||
Every command is executed directly from the root folder of the project unless specified otherwise.
|
||||
|
||||
1. Install prerequisites + pipenv as mentioned in
|
||||
1. Install prerequisites + [uv](https://github.com/astral-sh/uv) as mentioned in
|
||||
[Bare metal route](setup.md#bare_metal).
|
||||
|
||||
2. Copy `paperless.conf.example` to `paperless.conf` and enable debug
|
||||
@@ -75,26 +75,22 @@ first-time setup.
|
||||
4. Install the Python dependencies:
|
||||
|
||||
```bash
|
||||
pipenv install --dev
|
||||
$ uv sync --group dev
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Using a virtual environment is highly recommended. You can spawn one via `pipenv shell`.
|
||||
|
||||
5. Install pre-commit hooks:
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
$ uv run pre-commit install
|
||||
```
|
||||
|
||||
6. Apply migrations and create a superuser for your development instance:
|
||||
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
|
||||
|
||||
```bash
|
||||
# src/
|
||||
|
||||
python3 manage.py migrate
|
||||
python3 manage.py createsuperuser
|
||||
$ uv run manage.py migrate
|
||||
$ uv run manage.py createsuperuser
|
||||
```
|
||||
|
||||
7. You can now either ...
|
||||
@@ -144,7 +140,7 @@ To build the front end once use this command:
|
||||
```bash
|
||||
# src-ui/
|
||||
|
||||
$ npm install
|
||||
$ pnpm install
|
||||
$ ng build --configuration production
|
||||
```
|
||||
|
||||
@@ -164,10 +160,23 @@ $ ng build --configuration production
|
||||
complicated IF cases. Append `# noqa: E501` to disable this check
|
||||
for certain lines.
|
||||
|
||||
### Package Management
|
||||
|
||||
Paperless uses `uv` to manage packages and virtual environments for both development and production.
|
||||
To accomplish some common tasks using `uv`, follow the shortcuts below:
|
||||
|
||||
To upgrade all locked packages to the latest allowed versions: `uv lock --upgrade`
|
||||
|
||||
To upgrade a single locked package: `uv lock --upgrade-package <package>`
|
||||
|
||||
To add a new package: `uv add <package>`
|
||||
|
||||
To add a new development package `uv add --dev <package>`
|
||||
|
||||
## Front end development
|
||||
|
||||
The front end is built using AngularJS. In order to get started, you need Node.js (version 14.15+) and
|
||||
`npm`.
|
||||
`pnpm`.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -176,7 +185,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
||||
1. Install the Angular CLI. You might need sudo privileges to perform this command:
|
||||
|
||||
```bash
|
||||
npm install -g @angular/cli
|
||||
pnpm install -g @angular/cli
|
||||
```
|
||||
|
||||
2. Make sure that it's on your path.
|
||||
@@ -184,7 +193,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
||||
3. Install all necessary modules:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. You can launch a development server by running:
|
||||
@@ -198,7 +207,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
||||
restart it.
|
||||
|
||||
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts and CORS are in place so that the front end behaves exactly as in production.
|
||||
|
||||
### Testing and code style
|
||||
|
||||
@@ -332,27 +341,21 @@ LANGUAGES = [
|
||||
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
||||
If you want to build the documentation locally, this is how you do it:
|
||||
|
||||
1. Have an active pipenv shell (`pipenv shell`) and install Python dependencies:
|
||||
1. Build the documentation
|
||||
|
||||
```bash
|
||||
pipenv install --dev
|
||||
```
|
||||
|
||||
2. Build the documentation
|
||||
|
||||
```bash
|
||||
mkdocs build --config-file mkdocs.yml
|
||||
$ uv run mkdocs build --config-file mkdocs.yml
|
||||
```
|
||||
|
||||
_alternatively..._
|
||||
|
||||
3. Serve the documentation. This will spin up a
|
||||
2. Serve the documentation. This will spin up a
|
||||
copy of the documentation at http://127.0.0.1:8000
|
||||
that will automatically refresh every time you change
|
||||
something.
|
||||
|
||||
```bash
|
||||
mkdocs serve
|
||||
$ uv run mkdocs serve
|
||||
```
|
||||
|
||||
## Building the Docker image
|
||||
|
21
docs/faq.md
21
docs/faq.md
@@ -112,27 +112,6 @@ able to run paperless, you're a bit on your own. If you can't run the
|
||||
docker image, the documentation has instructions for bare metal
|
||||
installs.
|
||||
|
||||
## _How do I proxy this with NGINX?_
|
||||
|
||||
**A:** See [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx).
|
||||
|
||||
## _How do I get WebSocket support with Apache mod_wsgi_?
|
||||
|
||||
**A:** `mod_wsgi` by itself does not support ASGI. Paperless will
|
||||
continue to work with WSGI, but certain features such as status
|
||||
notifications about document consumption won't be available.
|
||||
|
||||
If you want to continue using `mod_wsgi`, you will have to run an
|
||||
ASGI-enabled web server as well that processes WebSocket connections,
|
||||
and configure Apache to redirect WebSocket connections to this server.
|
||||
Multiple options for ASGI servers exist:
|
||||
|
||||
- `gunicorn` with `uvicorn` as the worker implementation (the default
|
||||
of paperless)
|
||||
- `daphne` as a standalone server, which is the reference
|
||||
implementation for ASGI.
|
||||
- `uvicorn` as a standalone server
|
||||
|
||||
## _What about the Redis licensing change and using one of the open source forks_?
|
||||
|
||||
Currently (October 2024), forks of Redis such as Valkey or Redirect are not officially supported by our upstream
|
||||
|
@@ -197,7 +197,7 @@ People interested in continuing the work on paperless-ngx are encouraged to reac
|
||||
|
||||
### Translation
|
||||
|
||||
Paperless-ngx is available in many languages that are coordinated on [Crowdin](https://crwd.in/paperless-ngx). If you want to help out by translating paperless-ngx into your language, please head over to the [Paperless-ngx project at Crowdin](https://crwd.in/paperless-ngx), and thank you!
|
||||
Paperless-ngx is available in many languages that are coordinated on [Crowdin](https://crowdin.com/project/paperless-ngx). If you want to help out by translating paperless-ngx into your language, please head over to the [Paperless-ngx project at Crowdin](https://crowdin.com/project/paperless-ngx), and thank you!
|
||||
|
||||
## Scanners & Software
|
||||
|
||||
|
@@ -131,26 +131,11 @@ account. The script essentially automatically performs the steps described in [D
|
||||
by default but you can change the image to pull from Docker Hub by changing the `image`
|
||||
line to `image: paperlessngx/paperless-ngx:latest`.
|
||||
|
||||
6. To be able to login, you will need a "superuser". To create it,
|
||||
execute the following command:
|
||||
6. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||
|
||||
```shell-session
|
||||
docker compose run --rm webserver createsuperuser
|
||||
```
|
||||
|
||||
or using docker exec from within the container:
|
||||
|
||||
```shell-session
|
||||
python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
This will guide you through the superuser setup.
|
||||
|
||||
7. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||
|
||||
8. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
||||
(or similar, depending on your configuration). Use the superuser credentials you have
|
||||
created in the previous step to login.
|
||||
7. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
||||
(or similar, depending on your configuration). When you first access the web interface, you will be
|
||||
prompted to create a superuser account.
|
||||
|
||||
### Build the Docker image yourself {#docker_build}
|
||||
|
||||
@@ -380,16 +365,21 @@ are released, dependency support is confirmed, etc.
|
||||
dependencies. This is an alternative to the above and may require adjusting
|
||||
the example scripts to utilize the virtual environment paths
|
||||
|
||||
9. Go to `/opt/paperless/src`, and execute the following commands:
|
||||
!!! tip
|
||||
|
||||
If you use modern Python tooling, such as `uv`, installation will not include
|
||||
dependencies for Postgres or Mariadb. You can select those extras with `--extra <EXTRA>`
|
||||
or all with `--all-extras`
|
||||
|
||||
9. Go to `/opt/paperless/src`, and execute the following command:
|
||||
|
||||
```bash
|
||||
# This creates the database schema.
|
||||
sudo -Hu paperless python3 manage.py migrate
|
||||
|
||||
# This creates your first paperless user
|
||||
sudo -Hu paperless python3 manage.py createsuperuser
|
||||
```
|
||||
|
||||
When you first access the web interface you will be prompted to create a superuser account.
|
||||
|
||||
10. Optional: Test that paperless is working by executing
|
||||
|
||||
```bash
|
||||
@@ -426,31 +416,20 @@ are released, dependency support is confirmed, etc.
|
||||
|
||||
!!! note
|
||||
|
||||
The `socket` script enables `gunicorn` to run on port 80 without
|
||||
The `socket` script enables `granian` to run on port 80 without
|
||||
root privileges. For this you need to uncomment the
|
||||
`Require=paperless-webserver.socket` in the `webserver` script
|
||||
and configure `gunicorn` to listen on port 80 (see
|
||||
`paperless/gunicorn.conf.py`).
|
||||
|
||||
You may need to adjust the path to the `gunicorn` executable. This
|
||||
will be installed as part of the python dependencies, and is either
|
||||
located in the `bin` folder of your virtual environment, or in
|
||||
`~/.local/bin/` if no virtual environment is used.
|
||||
and configure `granian` to listen on port 80 (set `GRANIAN_PORT`).
|
||||
|
||||
These services rely on redis and optionally the database server, but
|
||||
don't need to be started in any particular order. The example files
|
||||
depend on redis being started. If you use a database server, you
|
||||
should add additional dependencies.
|
||||
|
||||
!!! warning
|
||||
!!! note
|
||||
|
||||
The included scripts run a `gunicorn` standalone server, which is
|
||||
fine for running paperless. It does support SSL, however, the
|
||||
documentation of GUnicorn states that you should use a proxy server
|
||||
in front of gunicorn instead.
|
||||
|
||||
For instructions on how to use nginx for that,
|
||||
[see the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx).
|
||||
For instructions on using a reverse proxy,
|
||||
[see the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#).
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -713,7 +692,8 @@ Paperless runs on Raspberry Pi. However, some things are rather slow on
|
||||
the Pi and configuring some options in paperless can help improve
|
||||
performance immensely:
|
||||
|
||||
- Stick with SQLite to save some resources.
|
||||
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
|
||||
if you encounter issues with SQLite locking.
|
||||
- If you do not need the filesystem-based consumer, consider disabling it
|
||||
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
|
||||
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
|
||||
|
@@ -195,34 +195,6 @@ This might have multiple reasons.
|
||||
is not, you need to compile the front end yourself or download the
|
||||
release archive instead of cloning the repository.
|
||||
|
||||
2. Check the output of the web server. You might see errors like this:
|
||||
|
||||
```
|
||||
[2021-01-25 10:08:04 +0000] [40] [ERROR] Socket error processing request.
|
||||
Traceback (most recent call last):
|
||||
File "/usr/local/lib/python3.7/site-packages/gunicorn/workers/sync.py", line 134, in handle
|
||||
self.handle_request(listener, req, client, addr)
|
||||
File "/usr/local/lib/python3.7/site-packages/gunicorn/workers/sync.py", line 190, in handle_request
|
||||
util.reraise(*sys.exc_info())
|
||||
File "/usr/local/lib/python3.7/site-packages/gunicorn/util.py", line 625, in reraise
|
||||
raise value
|
||||
File "/usr/local/lib/python3.7/site-packages/gunicorn/workers/sync.py", line 178, in handle_request
|
||||
resp.write_file(respiter)
|
||||
File "/usr/local/lib/python3.7/site-packages/gunicorn/http/wsgi.py", line 396, in write_file
|
||||
if not self.sendfile(respiter):
|
||||
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
|
||||
|
||||
```
|
||||
SENDFILE=0
|
||||
```
|
||||
|
||||
to your `docker-compose.env` file.
|
||||
|
||||
## Error while reading metadata
|
||||
|
||||
You might find messages like these in your log files:
|
||||
@@ -320,14 +292,16 @@ many workers attempting to access the database simultaneously.
|
||||
Consider changing to the PostgreSQL database if you will be processing
|
||||
many documents at once often. Otherwise, try tweaking the
|
||||
[`PAPERLESS_DB_TIMEOUT`](configuration.md#PAPERLESS_DB_TIMEOUT) setting to allow more time for the database to
|
||||
unlock. This may have minor performance implications.
|
||||
unlock. Additionally, you can change your SQLite database to use ["Write-Ahead Logging"](https://sqlite.org/wal.html).
|
||||
These changes may have minor performance implications but can help
|
||||
prevent database locking issues.
|
||||
|
||||
## gunicorn fails to start with "is not a valid port number"
|
||||
## granian fails to start with "is not a valid port number"
|
||||
|
||||
You are likely running using Kubernetes, which automatically creates an
|
||||
environment variable named `${serviceName}_PORT`. This is
|
||||
the same environment variable which is used by Paperless to optionally
|
||||
change the port gunicorn listens on.
|
||||
change the port granian listens on.
|
||||
|
||||
To fix this, set [`PAPERLESS_PORT`](configuration.md#PAPERLESS_PORT) again to your desired port, or the
|
||||
default of 8000.
|
||||
|
@@ -89,7 +89,7 @@ and more. These areas allow you to view, add, edit, delete and manage permission
|
||||
for these objects. You can also manage saved views, mail accounts, mail rules,
|
||||
workflows and more from the management sections.
|
||||
|
||||
## Adding documents to paperless
|
||||
## Adding documents to Paperless-ngx
|
||||
|
||||
Once you've got Paperless setup, you need to start feeding documents
|
||||
into it. When adding documents to paperless, it will perform the
|
||||
@@ -159,7 +159,7 @@ process.
|
||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects) for a user-maintained list of related projects and
|
||||
software (e.g. for mobile devices) that is compatible with Paperless-ngx.
|
||||
|
||||
### Email {#usage-email}
|
||||
### Incoming Email {#incoming-mail}
|
||||
|
||||
You can tell paperless-ngx to consume documents from your email
|
||||
accounts. This is a very flexible and powerful feature, if you regularly
|
||||
@@ -260,6 +260,31 @@ Once setup, navigating to the email settings page in Paperless-ngx will allow yo
|
||||
You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads)
|
||||
for details.
|
||||
|
||||
## Sharing documents from Paperless-ngx
|
||||
|
||||
Paperless-ngx supports sharing documents with other users by assigning them [permissions](#object-permissions)
|
||||
to the document. Document files can also be shared externally via [share links](#share-links), [email](#email-sharing)
|
||||
or using [email](#workflow-action-email) or [webhook](#workflow-action-webhook) actions in workflows.
|
||||
|
||||
### Share Links
|
||||
|
||||
"Share links" are shareable public links to files and can be created and managed under the 'Send' button on the document detail screen.
|
||||
|
||||
- Share links do not require a user to login and thus link directly to a file.
|
||||
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
||||
- Links can optionally have an expiration time set.
|
||||
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
|
||||
|
||||
!!! tip
|
||||
|
||||
If your paperless-ngx instance is behind a reverse-proxy you may want to create an exception to bypass any authentication layers that are part of your setup in order to make links truly publicly-accessible. Of course, do so with caution.
|
||||
|
||||
### Email Sharing {#email-sharing}
|
||||
|
||||
Paperless-ngx supports directly sending documents via email. If an email server has been [configured](configuration.md#email-sending)
|
||||
the "Send" button on the document detail page will include an "Email" option. You can also share files via email automatically by using
|
||||
a [workflow action](#workflow-action-email).
|
||||
|
||||
## Permissions
|
||||
|
||||
Permissions in Paperless-ngx are based around ['global' permissions](#global-permissions) as well as
|
||||
@@ -369,7 +394,7 @@ fields and permissions, which will be merged.
|
||||
|
||||
### Workflow Triggers
|
||||
|
||||
#### Types
|
||||
#### Types {#workflow-trigger-types}
|
||||
|
||||
Currently, there are three events that correspond to workflow trigger 'types':
|
||||
|
||||
@@ -429,11 +454,11 @@ Workflows allow you to filter by:
|
||||
|
||||
### Workflow Actions
|
||||
|
||||
#### Types
|
||||
#### Types {#workflow-action-types}
|
||||
|
||||
The following workflow action types are available:
|
||||
|
||||
##### Assignment
|
||||
##### Assignment {#workflow-action-assignment}
|
||||
|
||||
"Assignment" actions can assign:
|
||||
|
||||
@@ -443,7 +468,7 @@ The following workflow action types are available:
|
||||
- View and / or edit permissions to users or groups
|
||||
- Custom fields. Note that no value for the field will be set
|
||||
|
||||
##### Removal
|
||||
##### Removal {#workflow-action-removal}
|
||||
|
||||
"Removal" actions can remove either all of or specific sets of the following:
|
||||
|
||||
@@ -452,7 +477,7 @@ The following workflow action types are available:
|
||||
- View and / or edit permissions
|
||||
- Custom fields
|
||||
|
||||
##### Email
|
||||
##### Email {#workflow-action-email}
|
||||
|
||||
"Email" actions can send documents via email. This action requires a mail server to be [configured](configuration.md#email-sending). You can specify:
|
||||
|
||||
@@ -460,7 +485,7 @@ The following workflow action types are available:
|
||||
- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
|
||||
- Whether to include the document as an attachment
|
||||
|
||||
##### Webhook
|
||||
##### Webhook {#workflow-action-webhook}
|
||||
|
||||
"Webhook" actions send a POST request to a specified URL. You can specify:
|
||||
|
||||
@@ -544,19 +569,6 @@ The following custom field types are supported:
|
||||
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
|
||||
- `Select`: a pre-defined list of strings from which the user can choose
|
||||
|
||||
## Share Links
|
||||
|
||||
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
||||
|
||||
- Share links do not require a user to login and thus link directly to a file.
|
||||
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
||||
- Links can optionally have an expiration time set.
|
||||
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
|
||||
|
||||
!!! tip
|
||||
|
||||
If your paperless-ngx instance is behind a reverse-proxy you may want to create an exception to bypass any authentication layers that are part of your setup in order to make links truly publicly-accessible. Of course, do so with caution.
|
||||
|
||||
## PDF Actions
|
||||
|
||||
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
|
||||
@@ -837,7 +849,7 @@ Paperless-ngx consists of the following components:
|
||||
|
||||
```shell-session
|
||||
cd /path/to/paperless/src/
|
||||
gunicorn -c ../gunicorn.conf.py paperless.wsgi
|
||||
granian --interface asginl --ws "paperless.asgi:application"
|
||||
```
|
||||
|
||||
or by any other means such as Apache `mod_wsgi`.
|
||||
|
@@ -1,49 +0,0 @@
|
||||
import os
|
||||
|
||||
# See https://docs.gunicorn.org/en/stable/settings.html for
|
||||
# explanations of settings
|
||||
|
||||
bind = f"{os.getenv('PAPERLESS_BIND_ADDR', '[::]')}:{os.getenv('PAPERLESS_PORT', 8000)}"
|
||||
|
||||
workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 1))
|
||||
worker_class = "paperless.workers.ConfigurableWorker"
|
||||
timeout = 120
|
||||
preload_app = True
|
||||
|
||||
# https://docs.gunicorn.org/en/stable/faq.html#blocking-os-fchmod
|
||||
worker_tmp_dir = "/dev/shm"
|
||||
|
||||
|
||||
def pre_fork(server, worker):
|
||||
pass
|
||||
|
||||
|
||||
def pre_exec(server):
|
||||
server.log.info("Forked child, re-executing.")
|
||||
|
||||
|
||||
def when_ready(server):
|
||||
server.log.info("Server is ready. Spawning workers")
|
||||
|
||||
|
||||
def worker_int(worker):
|
||||
worker.log.info("worker received INT or QUIT signal")
|
||||
|
||||
## get traceback info
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
id2name = {th.ident: th.name for th in threading.enumerate()}
|
||||
code = []
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
code.append(f"\n# Thread: {id2name.get(threadId, '')}({threadId})")
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
code.append(f'File: "{filename}", line {lineno}, in {name}')
|
||||
if line:
|
||||
code.append(f" {line.strip()}")
|
||||
worker.log.debug("\n".join(code))
|
||||
|
||||
|
||||
def worker_abort(worker):
|
||||
worker.log.info("worker received SIGABRT signal")
|
355
pyproject.toml
Normal file
355
pyproject.toml
Normal file
@@ -0,0 +1,355 @@
|
||||
[project]
|
||||
name = "paperless-ngx"
|
||||
version = "2.15.3"
|
||||
description = "A community-supported supercharged version of paperless: scan, index and archive all your physical documents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
# TODO: Move certain things to groups and then utilize that further
|
||||
# This will allow testing to not install a webserver, mysql, etc
|
||||
|
||||
dependencies = [
|
||||
"bleach~=6.2.0",
|
||||
"celery[redis]~=5.5.1",
|
||||
"channels~=4.2",
|
||||
"channels-redis~=4.2",
|
||||
"concurrent-log-handler~=0.9.25",
|
||||
"dateparser~=1.2",
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
"django~=5.1.7",
|
||||
"django-allauth[socialaccount,mfa]~=65.4.0",
|
||||
"django-auditlog~=3.0.0",
|
||||
"django-celery-results~=2.5.1",
|
||||
"django-compression-middleware~=0.5.0",
|
||||
"django-cors-headers~=4.7.0",
|
||||
"django-extensions~=3.2.3",
|
||||
"django-filter~=25.1",
|
||||
"django-guardian~=2.4.0",
|
||||
"django-multiselectfield~=0.1.13",
|
||||
"django-soft-delete~=1.0.18",
|
||||
"djangorestframework~=3.15",
|
||||
"djangorestframework-guardian~=0.3.0",
|
||||
"drf-spectacular~=0.28",
|
||||
"drf-spectacular-sidecar~=2025.3.1",
|
||||
"drf-writable-nested~=0.7.1",
|
||||
"filelock~=3.17.0",
|
||||
"flower~=2.0.1",
|
||||
"gotenberg-client~=0.9.0",
|
||||
"httpx-oauth~=0.16",
|
||||
"imap-tools~=1.10.0",
|
||||
"inotifyrecursive~=0.3",
|
||||
"jinja2~=3.1.5",
|
||||
"langdetect~=1.0.9",
|
||||
"nltk~=3.9.1",
|
||||
"ocrmypdf~=16.10.0",
|
||||
"pathvalidate~=3.2.3",
|
||||
"pdf2image~=1.17.0",
|
||||
"python-dateutil~=2.9.0",
|
||||
"python-dotenv~=1.0.1",
|
||||
"python-gnupg~=0.5.4",
|
||||
"python-ipware~=3.0.0",
|
||||
"python-magic~=0.4.27",
|
||||
"pyzbar~=0.1.9",
|
||||
"rapidfuzz~=3.12.1",
|
||||
"redis[hiredis]~=5.2.1",
|
||||
"scikit-learn~=1.6.1",
|
||||
"setproctitle~=1.3.4",
|
||||
"tika-client~=0.9.0",
|
||||
"tqdm~=4.67.1",
|
||||
"watchdog~=6.0",
|
||||
"whitenoise~=6.9",
|
||||
"whoosh-reloaded>=2.7.5",
|
||||
"zxing-cpp~=2.3.0",
|
||||
]
|
||||
|
||||
optional-dependencies.mariadb = [
|
||||
"mysqlclient~=2.2.7",
|
||||
]
|
||||
optional-dependencies.postgres = [
|
||||
"psycopg[c]==3.2.5",
|
||||
# Direct dependency for proper resolution of the pre-built wheels
|
||||
"psycopg-c==3.2.5",
|
||||
]
|
||||
optional-dependencies.webserver = [
|
||||
"granian[uvloop]~=2.2.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
dev = [
|
||||
{ "include-group" = "docs" },
|
||||
{ "include-group" = "testing" },
|
||||
{ "include-group" = "lint" },
|
||||
]
|
||||
|
||||
docs = [
|
||||
"mkdocs-glightbox~=0.4.0",
|
||||
"mkdocs-material~=9.6.4",
|
||||
]
|
||||
|
||||
testing = [
|
||||
"daphne",
|
||||
"factory-boy~=3.3.1",
|
||||
"imagehash",
|
||||
"pytest~=8.3.3",
|
||||
"pytest-cov~=6.0.0",
|
||||
"pytest-django~=4.10.0",
|
||||
"pytest-env",
|
||||
"pytest-httpx",
|
||||
"pytest-mock",
|
||||
"pytest-rerunfailures",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist",
|
||||
]
|
||||
|
||||
lint = [
|
||||
"pre-commit~=4.1.0",
|
||||
"pre-commit-uv~=4.1.3",
|
||||
"ruff~=0.9.9",
|
||||
]
|
||||
|
||||
typing = [
|
||||
"celery-types",
|
||||
"django-filter-stubs",
|
||||
"django-stubs[compatible-mypy]",
|
||||
"djangorestframework-stubs[compatible-mypy]",
|
||||
"mypy",
|
||||
"types-bleach",
|
||||
"types-colorama",
|
||||
"types-dateparser",
|
||||
"types-markdown",
|
||||
"types-pygments",
|
||||
"types-python-dateutil",
|
||||
"types-redis",
|
||||
"types-setuptools",
|
||||
"types-tqdm",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 88
|
||||
src = [
|
||||
"src",
|
||||
]
|
||||
respect-gitignore = true
|
||||
# https://docs.astral.sh/ruff/settings/
|
||||
fix = true
|
||||
show-fixes = true
|
||||
|
||||
output-format = "grouped"
|
||||
# https://docs.astral.sh/ruff/rules/
|
||||
lint.extend-select = [
|
||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||
"EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
|
||||
"FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt
|
||||
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
||||
"G201", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
|
||||
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
||||
"ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
|
||||
"INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
|
||||
"ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
|
||||
"PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
|
||||
"PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
||||
"Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
|
||||
"RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
|
||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
||||
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
|
||||
"TC", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc
|
||||
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
|
||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
||||
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
||||
]
|
||||
lint.ignore = [
|
||||
"DJ001",
|
||||
"RUF012",
|
||||
"SIM105",
|
||||
]
|
||||
# Migrations
|
||||
lint.per-file-ignores."*/migrations/*.py" = [
|
||||
"E501",
|
||||
"SIM",
|
||||
"T201",
|
||||
]
|
||||
# Testing
|
||||
lint.per-file-ignores."*/tests/*.py" = [
|
||||
"E501",
|
||||
"SIM117",
|
||||
]
|
||||
lint.per-file-ignores.".github/scripts/*.py" = [
|
||||
"E501",
|
||||
"INP001",
|
||||
"SIM117",
|
||||
]
|
||||
# Docker specific
|
||||
lint.per-file-ignores."docker/rootfs/usr/local/bin/wait-for-redis.py" = [
|
||||
"INP001",
|
||||
"T201",
|
||||
]
|
||||
lint.per-file-ignores."docker/wait-for-redis.py" = [
|
||||
"INP001",
|
||||
"T201",
|
||||
]
|
||||
lint.per-file-ignores."src/documents/file_handling.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/management/commands/document_exporter.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/models.py" = [
|
||||
"SIM115",
|
||||
]
|
||||
lint.per-file-ignores."src/documents/parsers.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/signals/handlers.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_consumer.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_file_handling.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_management.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_management_consumer.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_management_exporter.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_migration_archive_files.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_migration_document_pages_count.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_migration_mime_type.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/tests/test_sanity_check.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/documents/views.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless/checks.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless/settings.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless/views.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless_mail/mail.py" = [
|
||||
"PTH",
|
||||
] # TODO Enable & remove
|
||||
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
||||
"PTH",
|
||||
"RUF001",
|
||||
] # TODO PTH Enable & remove
|
||||
lint.isort.force-single-line = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "8.0"
|
||||
pythonpath = [
|
||||
"src",
|
||||
]
|
||||
testpaths = [
|
||||
"src/documents/tests/",
|
||||
"src/paperless/tests/",
|
||||
"src/paperless_mail/tests/",
|
||||
"src/paperless_tesseract/tests/",
|
||||
"src/paperless_tika/tests",
|
||||
]
|
||||
addopts = [
|
||||
"--pythonwarnings=all",
|
||||
"--cov",
|
||||
"--cov-report=html",
|
||||
"--cov-report=xml",
|
||||
"--numprocesses=auto",
|
||||
"--maxprocesses=16",
|
||||
"--quiet",
|
||||
"--durations=50",
|
||||
"--junitxml=junit.xml",
|
||||
"-o junit_family=legacy",
|
||||
]
|
||||
norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ]
|
||||
|
||||
DJANGO_SETTINGS_MODULE = "paperless.settings"
|
||||
|
||||
[tool.pytest_env]
|
||||
PAPERLESS_DISABLE_DBHANDLER = "true"
|
||||
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = [
|
||||
"src/",
|
||||
]
|
||||
omit = [
|
||||
"*/tests/*",
|
||||
"manage.py",
|
||||
"paperless/wsgi.py",
|
||||
"paperless/auth.py",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_also = [
|
||||
"if settings.AUDIT_LOG_ENABLED:",
|
||||
"if AUDIT_LOG_ENABLED:",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
plugins = [
|
||||
"mypy_django_plugin.main",
|
||||
"mypy_drf_plugin.main",
|
||||
"numpy.typing.mypy_plugin",
|
||||
]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_defs = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
[tool.uv]
|
||||
required-version = ">=0.5.14"
|
||||
package = false
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
# Markers are chosen to select these almost exclusively when building the Docker image
|
||||
psycopg-c = [
|
||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
|
||||
]
|
||||
zxing-cpp = [
|
||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
|
||||
]
|
||||
|
||||
[tool.django-stubs]
|
||||
django_settings_module = "paperless.settings"
|
@@ -9,7 +9,21 @@ Requires=redis.service
|
||||
User=paperless
|
||||
Group=paperless
|
||||
WorkingDirectory=/opt/paperless/src
|
||||
ExecStart=/opt/paperless/.local/bin/gunicorn -c /opt/paperless/gunicorn.conf.py paperless.asgi:application
|
||||
|
||||
Environment=GRANIAN_HOST=::
|
||||
Environment=GRANIAN_PORT=8000
|
||||
Environment=GRANIAN_WORKERS=1
|
||||
|
||||
ExecStart=/bin/sh -c '\
|
||||
# Host: GRANIAN_HOST -> PAPERLESS_BIND_ADDR -> default \
|
||||
[ -n "$PAPERLESS_BIND_ADDR" ] && export GRANIAN_HOST=$PAPERLESS_BIND_ADDR; \
|
||||
# Port: GRANIAN_PORT -> PAPERLESS_PORT -> default \
|
||||
[ -n "$PAPERLESS_PORT" ] && export GRANIAN_PORT=$PAPERLESS_PORT; \
|
||||
# Workers: GRANIAN_WORKERS -> PAPERLESS_WEBSERVER_WORKERS -> default \
|
||||
[ -n "$PAPERLESS_WEBSERVER_WORKERS" ] && export GRANIAN_WORKERS=$PAPERLESS_WEBSERVER_WORKERS; \
|
||||
# URL path prefix: only set if PAPERLESS_FORCE_SCRIPT_NAME exists \
|
||||
[ -n "$PAPERLESS_FORCE_SCRIPT_NAME" ] && export GRANIAN_URL_PATH_PREFIX=$PAPERLESS_FORCE_SCRIPT_NAME; \
|
||||
exec granian --interface asginl --ws "paperless.asgi:application"'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
1
src-ui/.npmrc
Normal file
1
src-ui/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
shamefully-hoist=true
|
@@ -178,7 +178,8 @@
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
],
|
||||
"analytics": false
|
||||
"analytics": false,
|
||||
"packageManager": "pnpm"
|
||||
},
|
||||
"schematics": {
|
||||
"@angular-eslint/schematics:application": {
|
||||
|
@@ -83,10 +83,17 @@ test('date filtering', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.getByRole('button', { name: 'Dates' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Within 3 months' }).first().click()
|
||||
await page.locator('.ng-arrow-wrapper').first().click()
|
||||
await page.getByRole('option', { name: 'Within 3 months' }).click()
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
|
||||
await page.getByRole('menuitem', { name: 'Within 3 months' }).first().click()
|
||||
await page.getByLabel('Datesselected').getByRole('button').first().click()
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'Relative dates' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click()
|
||||
await page.getByRole('option', { name: 'Within 3 months' }).click()
|
||||
await page.getByLabel('Dates selected').locator('button').first().click()
|
||||
await page.getByLabel('Dates selected').locator('button').first().click()
|
||||
await page.getByRole('combobox', { name: 'Select month' }).selectOption('12')
|
||||
await page.getByRole('combobox', { name: 'Select year' }).selectOption('2022')
|
||||
await page.getByText('11', { exact: true }).click()
|
||||
|
@@ -7,9 +7,20 @@ module.exports = {
|
||||
'abstract-name-filter-service',
|
||||
'abstract-paperless-service',
|
||||
],
|
||||
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
|
||||
transformIgnorePatterns: [
|
||||
`<rootDir>/node_modules/.pnpm/(?!.*\\.mjs$|lodash-es)`,
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
workerIdleMemoryLimit: '512MB',
|
||||
reporters: [
|
||||
'default',
|
||||
[
|
||||
'jest-junit',
|
||||
{
|
||||
classNameTemplate: '{filepath}/{classname}: {title}',
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
1093
src-ui/messages.xlf
1093
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
19090
src-ui/package-lock.json
generated
19090
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "paperless-ui",
|
||||
"version": "0.0.0",
|
||||
"name": "paperless-ngx-ui",
|
||||
"version": "2.15.3",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
@@ -11,17 +12,17 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.1.2",
|
||||
"@angular/common": "~19.1.4",
|
||||
"@angular/compiler": "~19.1.4",
|
||||
"@angular/core": "~19.1.4",
|
||||
"@angular/forms": "~19.1.4",
|
||||
"@angular/localize": "~19.1.4",
|
||||
"@angular/platform-browser": "~19.1.4",
|
||||
"@angular/platform-browser-dynamic": "~19.1.4",
|
||||
"@angular/router": "~19.1.4",
|
||||
"@angular/cdk": "^19.2.7",
|
||||
"@angular/common": "~19.2.4",
|
||||
"@angular/compiler": "~19.2.4",
|
||||
"@angular/core": "~19.2.4",
|
||||
"@angular/forms": "~19.2.4",
|
||||
"@angular/localize": "~19.2.4",
|
||||
"@angular/platform-browser": "~19.2.4",
|
||||
"@angular/platform-browser-dynamic": "~19.2.4",
|
||||
"@angular/router": "~19.2.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||
"@ng-select/ng-select": "^14.2.0",
|
||||
"@ng-select/ng-select": "^14.2.6",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
@@ -29,46 +30,56 @@
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^19.1.0",
|
||||
"ngx-color": "^10.0.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"ngx-device-detector": "^9.0.0",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"utif": "^3.1.0",
|
||||
"uuid": "^11.0.5",
|
||||
"uuid": "^11.1.0",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^19.0.0",
|
||||
"@angular-builders/jest": "^19.0.0",
|
||||
"@angular-devkit/build-angular": "^19.0.4",
|
||||
"@angular-devkit/core": "^19.1.5",
|
||||
"@angular-devkit/schematics": "^19.1.5",
|
||||
"@angular-eslint/builder": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.2",
|
||||
"@angular-eslint/schematics": "19.0.2",
|
||||
"@angular-eslint/template-parser": "19.0.2",
|
||||
"@angular/cli": "~19.1.5",
|
||||
"@angular/compiler-cli": "~19.1.4",
|
||||
"@codecov/webpack-plugin": "^1.8.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@angular-devkit/build-angular": "^19.2.5",
|
||||
"@angular-devkit/core": "^19.2.5",
|
||||
"@angular-devkit/schematics": "^19.2.5",
|
||||
"@angular-eslint/builder": "19.3.0",
|
||||
"@angular-eslint/eslint-plugin": "19.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.3.0",
|
||||
"@angular-eslint/schematics": "19.3.0",
|
||||
"@angular-eslint/template-parser": "19.3.0",
|
||||
"@angular/cli": "~19.2.5",
|
||||
"@angular/compiler-cli": "~19.2.4",
|
||||
"@codecov/webpack-plugin": "^1.9.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@typescript-eslint/utils": "^8.0.0",
|
||||
"eslint": "^9.19.0",
|
||||
"@types/node": "^22.13.17",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"@typescript-eslint/utils": "^8.29.0",
|
||||
"eslint": "^9.23.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^14.4.2",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "^14.5.4",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"canvas",
|
||||
"esbuild",
|
||||
"lmdb",
|
||||
"msgpackr-extract"
|
||||
]
|
||||
},
|
||||
"typings": "./src/typings.d.ts"
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
port,
|
||||
command: 'npm run start',
|
||||
command: 'pnpm run start',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 2 * 60 * 1000,
|
||||
},
|
||||
|
12438
src-ui/pnpm-lock.yaml
generated
Normal file
12438
src-ui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,13 @@ export const routes: Routes = [
|
||||
component: AppFrameComponent,
|
||||
canDeactivate: [DirtyDocGuard],
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
data: {
|
||||
componentName: 'AppFrameComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'documents',
|
||||
component: DocumentListComponent,
|
||||
@@ -47,6 +53,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
componentName: 'DocumentListComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -59,6 +66,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.SavedView,
|
||||
},
|
||||
componentName: 'DocumentListComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -70,6 +78,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
componentName: 'DocumentDetailComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -81,6 +90,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
componentName: 'DocumentDetailComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -92,6 +102,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
componentName: 'DocumentAsnComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -103,6 +114,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Tag,
|
||||
},
|
||||
componentName: 'TagListComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -114,6 +126,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.DocumentType,
|
||||
},
|
||||
componentName: 'DocumentTypeListComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -125,6 +138,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Correspondent,
|
||||
},
|
||||
componentName: 'CorrespondentListComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -136,6 +150,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.StoragePath,
|
||||
},
|
||||
componentName: 'StoragePathListComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -144,6 +159,7 @@ export const routes: Routes = [
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requireAdmin: true,
|
||||
componentName: 'LogsComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -155,6 +171,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.Delete,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
componentName: 'TrashComponent',
|
||||
},
|
||||
},
|
||||
// redirect old paths
|
||||
@@ -180,6 +197,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.Change,
|
||||
type: PermissionType.UISettings,
|
||||
},
|
||||
componentName: 'SettingsComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -192,6 +210,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.UISettings,
|
||||
},
|
||||
componentName: 'SettingsComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -203,6 +222,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.Change,
|
||||
type: PermissionType.AppConfig,
|
||||
},
|
||||
componentName: 'ConfigComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -214,6 +234,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.PaperlessTask,
|
||||
},
|
||||
componentName: 'TasksComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -225,6 +246,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.CustomField,
|
||||
},
|
||||
componentName: 'CustomFieldsComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -236,6 +258,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Workflow,
|
||||
},
|
||||
componentName: 'WorkflowsComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -247,6 +270,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.MailAccount,
|
||||
},
|
||||
componentName: 'MailComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -258,6 +282,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.User,
|
||||
},
|
||||
componentName: 'UsersAndGroupsComponent',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -269,6 +294,7 @@ export const routes: Routes = [
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.SavedView,
|
||||
},
|
||||
componentName: 'SavedViewsComponent',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Sidebar</span>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Dark mode</span>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@
|
||||
<p i18n>
|
||||
Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually.
|
||||
</p>
|
||||
<p>
|
||||
<p class="mb-0">
|
||||
<em i18n>No tracking data is collected by the app in any way.</em>
|
||||
</p>
|
||||
</ng-template>
|
||||
@@ -173,7 +173,7 @@
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Saved Views</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||
</div>
|
||||
@@ -183,15 +183,15 @@
|
||||
<div class="col-xl-6 ps-xl-5">
|
||||
<h5 class="mt-3 mt-md-0" i18n>Document editing</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
<span i18n>Default zoom:</span>
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default zoom</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
||||
@@ -202,7 +202,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||
</div>
|
||||
@@ -214,10 +214,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Notes</h5>
|
||||
<div class="row mb-3">
|
||||
<h5 class="mt-3" i18n>Global search</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Full search links to</span>
|
||||
</div>
|
||||
<div class="col mb-3">
|
||||
<select class="form-select" formControlName="searchLink">
|
||||
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
|
||||
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -229,26 +241,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Global search</h5>
|
||||
<h5 class="mt-3" i18n>Notes</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Full search links to</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<select class="form-select" formControlName="searchLink">
|
||||
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
|
||||
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -267,8 +263,8 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<p i18n>
|
||||
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
|
||||
</p>
|
||||
Settings apply to this user account for objects (Tags, Mail Rules, etc. but not documents) created via the web UI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
@@ -307,7 +303,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Default Edit Permissions</span>
|
||||
</div>
|
||||
@@ -346,7 +342,7 @@
|
||||
|
||||
<h5 i18n>Document processing</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
|
||||
|
@@ -303,12 +303,17 @@ describe('SettingsComponent', () => {
|
||||
redis_error:
|
||||
'Error 61 connecting to localhost:6379. Connection refused.',
|
||||
celery_status: SystemStatusItemStatus.ERROR,
|
||||
celery_url: 'celery@localhost',
|
||||
celery_error: 'Error connecting to celery@localhost',
|
||||
index_status: SystemStatusItemStatus.OK,
|
||||
index_last_modified: new Date().toISOString(),
|
||||
index_error: null,
|
||||
classifier_status: SystemStatusItemStatus.OK,
|
||||
classifier_last_trained: new Date().toISOString(),
|
||||
classifier_error: null,
|
||||
sanity_check_status: SystemStatusItemStatus.ERROR,
|
||||
sanity_check_last_run: new Date().toISOString(),
|
||||
sanity_check_error: 'Error running sanity check.',
|
||||
},
|
||||
}
|
||||
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||
@@ -320,6 +325,8 @@ describe('SettingsComponent', () => {
|
||||
component['systemStatus'].database.status = SystemStatusItemStatus.OK
|
||||
component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
|
||||
component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
|
||||
component['systemStatus'].tasks.sanity_check_status =
|
||||
SystemStatusItemStatus.OK
|
||||
expect(component.systemStatusHasErrors).toBeFalsy()
|
||||
})
|
||||
|
||||
|
@@ -164,7 +164,10 @@ export class SettingsComponent
|
||||
this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
|
||||
this.systemStatus.tasks.classifier_status ===
|
||||
SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.sanity_check_status ===
|
||||
SystemStatusItemStatus.ERROR
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -19,6 +19,7 @@ import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskName,
|
||||
PaperlessTaskStatus,
|
||||
PaperlessTaskType,
|
||||
} from 'src/app/data/paperless-task'
|
||||
@@ -39,7 +40,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'test.pdf',
|
||||
date_created: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
date_done: new Date('2023-03-01T10:26:07.223048Z'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
|
||||
acknowledged: false,
|
||||
@@ -51,7 +53,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: '191092.pdf',
|
||||
date_created: new Date('2023-03-01T09:26:03.093116Z'),
|
||||
date_done: new Date('2023-03-01T09:26:07.223048Z'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
result:
|
||||
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
|
||||
@@ -64,7 +67,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
|
||||
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
|
||||
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Pending,
|
||||
result: null,
|
||||
acknowledged: false,
|
||||
@@ -76,7 +80,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'paperless-mail-l4dkg8ir',
|
||||
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
|
||||
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
result: 'Success. New document id 422 created',
|
||||
acknowledged: false,
|
||||
@@ -88,7 +93,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'onlinePaymentSummary.pdf',
|
||||
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
|
||||
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
result: 'Success. New document id 421 created',
|
||||
acknowledged: false,
|
||||
@@ -100,7 +106,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'paperless-mail-_rrpmqk6',
|
||||
date_created: new Date('2023-06-07T02:54:35.694916Z'),
|
||||
date_done: null,
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Started,
|
||||
result: null,
|
||||
acknowledged: false,
|
||||
@@ -155,7 +162,9 @@ describe('TasksComponent', () => {
|
||||
jest.useFakeTimers()
|
||||
fixture.detectChanges()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}tasks/`)
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
.flush(tasks)
|
||||
})
|
||||
|
||||
|
@@ -15,7 +15,7 @@
|
||||
</svg>
|
||||
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
|
||||
@if (customAppTitle?.length) {
|
||||
<div class="d-flex flex-column align-items-start">
|
||||
<div class="d-flex flex-column align-items-start custom-title">
|
||||
<span class="title">{{customAppTitle}}</span>
|
||||
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
||||
</div>
|
||||
|
@@ -244,7 +244,7 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (min-width: 366px) and (max-width: 768px) {
|
||||
.navbar-toggler {
|
||||
// compensate for 2 buttons on the right
|
||||
margin-right: 45px;
|
||||
@@ -257,6 +257,13 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 345px) {
|
||||
.custom-title {
|
||||
max-width: 110px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .dropdown.show .dropdown-toggle,
|
||||
:host ::ng-deep .dropdown-toggle:hover {
|
||||
opacity: 0.7;
|
||||
|
@@ -89,7 +89,7 @@
|
||||
@if (searchResults?.documents.length) {
|
||||
<h6 class="dropdown-header" i18n="@@searchResults.documents">Documents</h6>
|
||||
@for (document of searchResults.documents; track document.id) {
|
||||
<ng-container *ngTemplateOutlet="resultItemTemplate; context: {item: document, nameProp: 'title', type: DataType.Document, icon: 'file-text', date: document.added}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="resultItemTemplate; context: {item: document, nameProp: 'title', type: DataType.Document, icon: 'file-text', date: document.created}"></ng-container>
|
||||
}
|
||||
}
|
||||
@if (searchResults?.saved_views.length) {
|
||||
|
@@ -21,7 +21,7 @@
|
||||
}
|
||||
<div class="scroll-list">
|
||||
@for (toast of toasts; track toast.id) {
|
||||
<pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (close)="toastService.closeToast(toast)"></pngx-toast>
|
||||
<pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (closed)="toastService.closeToast(toast)"></pngx-toast>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -28,10 +28,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="archiveFallbackSwitch" [(ngModel)]="archiveFallback">
|
||||
<label class="form-check-label" for="archiveFallbackSwitch" i18n>Try to include archive version in merge for non-PDF files</label>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalsSwitch" [(ngModel)]="deleteOriginals" [disabled]="!userOwnsAllDocuments">
|
||||
<label class="form-check-label" for="deleteOriginalsSwitch" i18n>Delete original documents after successful merge</label>
|
||||
</div>
|
||||
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
|
||||
@if (!archiveFallback) {
|
||||
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
|
@@ -29,6 +29,7 @@ export class MergeConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
public documentIDs: number[] = []
|
||||
public archiveFallback: boolean = false
|
||||
public deleteOriginals: boolean = false
|
||||
private _documents: Document[] = []
|
||||
get documents(): Document[] {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)">
|
||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions" placement="bottom-end">
|
||||
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
|
||||
<i-bs name="ui-radios"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
|
||||
@@ -36,6 +37,8 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
|
||||
],
|
||||
})
|
||||
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
@Input()
|
||||
documentId: number
|
||||
|
||||
|
@@ -34,7 +34,7 @@ import {
|
||||
CustomFieldQueryElement,
|
||||
CustomFieldQueryExpression,
|
||||
} from 'src/app/utils/custom-field-query-element'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||
@@ -183,7 +183,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
public CUSTOM_FIELD_QUERY_MAX_DEPTH = CUSTOM_FIELD_QUERY_MAX_DEPTH
|
||||
public CUSTOM_FIELD_QUERY_MAX_ATOMS = CUSTOM_FIELD_QUERY_MAX_ATOMS
|
||||
public popperOptions = popperOptionsReenablePreventOverflow
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
@@ -1,161 +1,158 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group" [popperOptions]="popperOptions">
|
||||
<div class="btn-group w-100" ngbDropdown role="group" [popperOptions]="popperOptions" [placement]="placement">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="createdDateTo || createdDateFrom ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||
</button>
|
||||
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="row d-flex">
|
||||
<div class="col border-end">
|
||||
<div class="list-group list-group-flush">
|
||||
<h6 class="dropdown-header border-bottom" i18n>Created</h6>
|
||||
@for (rd of relativeDates; track rd) {
|
||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setCreatedRelativeDate(rd.id)">
|
||||
<div class="selected-icon">
|
||||
@if (createdRelativeDate === rd.id) {
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
<span class="small">
|
||||
{{ rd.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<h6 class="dropdown-header border-bottom" i18n>Created</h6>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item d-flex p-2 select-item" role="menuitem">
|
||||
<div class="selected-icon">
|
||||
@if (createdRelativeDate) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedRelativeDate()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="selected-icon">
|
||||
@if (createdDateFrom) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedFrom()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>From</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateFrom" ngbDatepicker #createdDateFromPicker="ngbDatepicker" [footerTemplate]="createdFromFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateFromPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #createdFromFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="createdDateFrom = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="createdDateFromPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="selected-icon">
|
||||
@if (createdDateTo) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedTo()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>To</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateTo" ngbDatepicker #createdDateToPicker="ngbDatepicker" [footerTemplate]="createdToFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateToPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #createdToFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="createdDateTo = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="createdDateToPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<ng-select class="w-100" name="createdRelativeDate"
|
||||
[items]="relativeDates" [(ngModel)]="createdRelativeDate"
|
||||
bindValue="id"
|
||||
bindLabel="name"
|
||||
clearable="false"
|
||||
placeholder="Relative dates"
|
||||
i18n-placeholder
|
||||
(change)="onSetCreatedRelativeDate($event)">
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<div class="d-flex">{{ item.name }}<span class="ms-auto text-muted small">{{ item.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container></span></div>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h6 class="dropdown-header border-bottom" i18n>Added</h6>
|
||||
<div class="list-group list-group-flush">
|
||||
@for (rd of relativeDates; track rd) {
|
||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setAddedRelativeDate(rd.id)">
|
||||
<div class="selected-icon">
|
||||
@if (addedRelativeDate === rd.id) {
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
<span class="small">
|
||||
{{ rd.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
<div class="selected-icon">
|
||||
@if (createdDateFrom) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedFrom()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="selected-icon">
|
||||
@if (addedDateFrom) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedFrom()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>From</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateFrom" ngbDatepicker #createdDateFromPicker="ngbDatepicker" [footerTemplate]="createdFromFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateFromPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #createdFromFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="createdDateFrom = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="createdDateFromPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>From</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateFrom" ngbDatepicker #addedDateFromPicker="ngbDatepicker" [footerTemplate]="addedFromFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateFromPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #addedFromFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="addedDateFrom = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="addedDateFromPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
<div class="selected-icon">
|
||||
@if (createdDateTo) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedTo()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>To</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateTo" ngbDatepicker #createdDateToPicker="ngbDatepicker" [footerTemplate]="createdToFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateToPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #createdToFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="createdDateTo = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="createdDateToPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="selected-icon">
|
||||
@if (addedDateTo) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedTo()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<h6 class="dropdown-header border-bottom" i18n>Added</h6>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item d-flex p-2 select-item" role="menuitem">
|
||||
<div class="selected-icon">
|
||||
@if (addedRelativeDate) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedRelativeDate()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<ng-select class="w-100" name="addedRelativeDate"
|
||||
[items]="relativeDates" [(ngModel)]="addedRelativeDate"
|
||||
bindValue="id"
|
||||
bindLabel="name"
|
||||
clearable="false"
|
||||
placeholder="Relative dates"
|
||||
i18n-placeholder
|
||||
(change)="onSetAddedRelativeDate($event)">
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<div class="d-flex">{{ item.name }}<span class="ms-auto text-muted small">{{ item.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container></span></div>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
<div class="selected-icon">
|
||||
@if (addedDateFrom) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedFrom()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>From</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateFrom" ngbDatepicker #addedDateFromPicker="ngbDatepicker" [footerTemplate]="addedFromFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateFromPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #addedFromFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="addedDateFrom = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="addedDateFromPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>To</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateTo" ngbDatepicker #addedDateToPicker="ngbDatepicker" [footerTemplate]="addedToFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateToPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #addedToFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="addedDateTo = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="addedDateToPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
<div class="selected-icon">
|
||||
@if (addedDateTo) {
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedTo()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>To</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateTo" ngbDatepicker #addedDateToPicker="ngbDatepicker" [footerTemplate]="addedToFooterTemplate">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateToPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
<ng-template #addedToFooterTemplate>
|
||||
<div class="btn-group-xs border-top p-2 d-flex">
|
||||
<button class="btn btn-primary" (click)="addedDateTo = today; onChangeDebounce()" i18n>Today</button>
|
||||
<button class="btn btn-secondary ms-auto" (click)="addedDateToPicker.close()" i18n>Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,16 +1,7 @@
|
||||
.date-dropdown {
|
||||
--bs-dropdown-min-width: 22rem;
|
||||
white-space: nowrap;
|
||||
|
||||
@media(min-width: 768px) {
|
||||
--bs-dropdown-min-width: 40rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.border-end {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -21,6 +12,10 @@
|
||||
min-height: 1em;
|
||||
}
|
||||
|
||||
.select-item .selected-icon {
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.input-group-sm {
|
||||
.form-control {
|
||||
font-size: 0.875rem;
|
||||
|
@@ -82,10 +82,12 @@ describe('DatesDropdownComponent', () => {
|
||||
it('should support relative dates', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
component.setCreatedRelativeDate(null)
|
||||
component.setCreatedRelativeDate(RelativeDate.WITHIN_1_WEEK)
|
||||
component.setAddedRelativeDate(null)
|
||||
component.setAddedRelativeDate(RelativeDate.WITHIN_1_WEEK)
|
||||
component.createdRelativeDate = RelativeDate.WITHIN_1_WEEK // normally set by ngModel binding in dropdown
|
||||
component.onSetCreatedRelativeDate({
|
||||
id: RelativeDate.WITHIN_1_WEEK,
|
||||
} as any)
|
||||
component.addedRelativeDate = RelativeDate.WITHIN_1_WEEK // normally set by ngModel binding in dropdown
|
||||
component.onSetAddedRelativeDate({ id: RelativeDate.WITHIN_1_WEEK } as any)
|
||||
tick(500)
|
||||
expect(result).toEqual({
|
||||
createdFrom: null,
|
||||
@@ -147,8 +149,19 @@ describe('DatesDropdownComponent', () => {
|
||||
expect(component.addedDateTo).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearRelativeDate', () => {
|
||||
component.createdRelativeDate = RelativeDate.WITHIN_1_WEEK
|
||||
component.clearCreatedRelativeDate()
|
||||
expect(component.createdRelativeDate).toBeNull()
|
||||
|
||||
component.addedRelativeDate = RelativeDate.WITHIN_1_WEEK
|
||||
component.clearAddedRelativeDate()
|
||||
expect(component.addedRelativeDate).toBeNull()
|
||||
})
|
||||
|
||||
it('should limit keyboard events', () => {
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
const input: HTMLInputElement =
|
||||
fixture.nativeElement.querySelector('input.form-control')
|
||||
let event: KeyboardEvent = new KeyboardEvent('keypress', {
|
||||
key: '9',
|
||||
})
|
||||
@@ -163,4 +176,19 @@ describe('DatesDropdownComponent', () => {
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support debounce', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
component.onChangeDebounce()
|
||||
tick(500)
|
||||
expect(result).toEqual({
|
||||
createdFrom: null,
|
||||
createdTo: null,
|
||||
createdRelativeDateID: null,
|
||||
addedFrom: null,
|
||||
addedTo: null,
|
||||
addedRelativeDateID: null,
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
@@ -13,13 +13,14 @@ import {
|
||||
NgbDatepickerModule,
|
||||
NgbDropdownModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
|
||||
export interface DateSelection {
|
||||
@@ -32,10 +33,14 @@ export interface DateSelection {
|
||||
}
|
||||
|
||||
export enum RelativeDate {
|
||||
WITHIN_1_WEEK = 0,
|
||||
WITHIN_1_MONTH = 1,
|
||||
WITHIN_3_MONTHS = 2,
|
||||
WITHIN_1_YEAR = 3,
|
||||
WITHIN_1_WEEK = 1,
|
||||
WITHIN_1_MONTH = 2,
|
||||
WITHIN_3_MONTHS = 3,
|
||||
WITHIN_1_YEAR = 4,
|
||||
THIS_YEAR = 5,
|
||||
THIS_MONTH = 6,
|
||||
TODAY = 7,
|
||||
YESTERDAY = 8,
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -49,13 +54,14 @@ export enum RelativeDate {
|
||||
NgxBootstrapIconsModule,
|
||||
NgbDatepickerModule,
|
||||
NgbDropdownModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgClass,
|
||||
],
|
||||
})
|
||||
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
public popperOptions = popperOptionsReenablePreventOverflow
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
constructor(settings: SettingsService) {
|
||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||
@@ -82,44 +88,64 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
name: $localize`Within 1 year`,
|
||||
date: new Date().setFullYear(new Date().getFullYear() - 1),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.THIS_YEAR,
|
||||
name: $localize`This year`,
|
||||
date: new Date('1/1/' + new Date().getFullYear()),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.THIS_MONTH,
|
||||
name: $localize`This month`,
|
||||
date: new Date().setDate(1),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.TODAY,
|
||||
name: $localize`Today`,
|
||||
date: new Date().setHours(0, 0, 0, 0),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.YESTERDAY,
|
||||
name: $localize`Yesterday`,
|
||||
date: new Date().setDate(new Date().getDate() - 1),
|
||||
},
|
||||
]
|
||||
|
||||
datePlaceHolder: string
|
||||
|
||||
// created
|
||||
@Input()
|
||||
createdDateTo: string
|
||||
createdDateTo: string = null
|
||||
|
||||
@Output()
|
||||
createdDateToChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
createdDateFrom: string
|
||||
createdDateFrom: string = null
|
||||
|
||||
@Output()
|
||||
createdDateFromChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
createdRelativeDate: RelativeDate
|
||||
createdRelativeDate: RelativeDate = null
|
||||
|
||||
@Output()
|
||||
createdRelativeDateChange = new EventEmitter<number>()
|
||||
|
||||
// added
|
||||
@Input()
|
||||
addedDateTo: string
|
||||
addedDateTo: string = null
|
||||
|
||||
@Output()
|
||||
addedDateToChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
addedDateFrom: string
|
||||
addedDateFrom: string = null
|
||||
|
||||
@Output()
|
||||
addedDateFromChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
addedRelativeDate: RelativeDate
|
||||
addedRelativeDate: RelativeDate = null
|
||||
|
||||
@Output()
|
||||
addedRelativeDateChange = new EventEmitter<number>()
|
||||
@@ -133,6 +159,9 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
@Input()
|
||||
placement: string = 'bottom-start'
|
||||
|
||||
public readonly today: string = new Date().toISOString().split('T')[0]
|
||||
|
||||
get isActive(): boolean {
|
||||
@@ -172,17 +201,17 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
setCreatedRelativeDate(rd: RelativeDate) {
|
||||
onSetCreatedRelativeDate(rd: { id: number; name: string; date: number }) {
|
||||
// createdRelativeDate is set by ngModel
|
||||
this.createdDateTo = null
|
||||
this.createdDateFrom = null
|
||||
this.createdRelativeDate = this.createdRelativeDate == rd ? null : rd
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
setAddedRelativeDate(rd: RelativeDate) {
|
||||
onSetAddedRelativeDate(rd: { id: number; name: string; date: number }) {
|
||||
// addedRelativeDate is set by ngModel
|
||||
this.addedDateTo = null
|
||||
this.addedDateFrom = null
|
||||
this.addedRelativeDate = this.addedRelativeDate == rd ? null : rd
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
@@ -224,6 +253,11 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearCreatedRelativeDate() {
|
||||
this.createdRelativeDate = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearAddedTo() {
|
||||
this.addedDateTo = null
|
||||
this.onChange()
|
||||
@@ -234,6 +268,11 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearAddedRelativeDate() {
|
||||
this.addedRelativeDate = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
// prevent chars other than numbers and separators
|
||||
onKeyPress(event: KeyboardEvent) {
|
||||
if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) {
|
||||
|
@@ -9,24 +9,19 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<pngx-input-text [horizontal]="true" i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<pngx-input-select [horizontal]="true" i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<pngx-input-number [horizontal]="true" i18n-title title="Order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
</div>
|
||||
<div class="col-md-2 pt-2">
|
||||
<pngx-input-switch [horizontal]="true" i18n-title title="Enabled" formControlName="enabled"></pngx-input-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<pngx-input-number [horizontal]="true" i18n-title title="Order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<pngx-input-switch [horizontal]="true" i18n-title title="Stop further processing" formControlName="stop_processing" i18n-hint hint="Stop processing further rules if this rule queues any document(s)."></pngx-input-switch>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-0"/>
|
||||
<div class="row">
|
||||
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the criteria specified below.</p>
|
||||
|
@@ -221,7 +221,6 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
|
||||
),
|
||||
assign_correspondent: new FormControl(null),
|
||||
assign_owner_from_rule: new FormControl(true),
|
||||
stop_processing: new FormControl(false),
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -189,6 +189,7 @@
|
||||
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
|
||||
<pngx-input-custom-fields-values formControlName="assign_custom_fields_values" [selectedFields]="formGroup.get('assign_custom_fields').value" (removeSelectedField)="removeSelectedCustomField($event, formGroup)"></pngx-input-custom-fields-values>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||
|
@@ -2,7 +2,12 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { of } from 'rxjs'
|
||||
@@ -369,4 +374,19 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
expect(component.objectForm.get('actions').value[0].email).toBeNull()
|
||||
expect(component.objectForm.get('actions').value[0].webhook).toBeNull()
|
||||
})
|
||||
|
||||
it('should remove selected custom field from the form group', () => {
|
||||
const formGroup = new FormGroup({
|
||||
assign_custom_fields: new FormControl([1, 2, 3]),
|
||||
})
|
||||
|
||||
component.removeSelectedCustomField(2, formGroup)
|
||||
expect(formGroup.get('assign_custom_fields').value).toEqual([1, 3])
|
||||
|
||||
component.removeSelectedCustomField(1, formGroup)
|
||||
expect(formGroup.get('assign_custom_fields').value).toEqual([3])
|
||||
|
||||
component.removeSelectedCustomField(3, formGroup)
|
||||
expect(formGroup.get('assign_custom_fields').value).toEqual([])
|
||||
})
|
||||
})
|
||||
|
@@ -47,6 +47,7 @@ import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
|
||||
import { CheckComponent } from '../../input/check/check.component'
|
||||
import { CustomFieldsValuesComponent } from '../../input/custom-fields-values/custom-fields-values.component'
|
||||
import { EntriesComponent } from '../../input/entries/entries.component'
|
||||
import { NumberComponent } from '../../input/number/number.component'
|
||||
import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component'
|
||||
@@ -151,6 +152,7 @@ const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
|
||||
SelectComponent,
|
||||
TextAreaComponent,
|
||||
TagsComponent,
|
||||
CustomFieldsValuesComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
ConfirmButtonComponent,
|
||||
@@ -439,6 +441,9 @@ export class WorkflowEditDialogComponent
|
||||
assign_change_users: new FormControl(action.assign_change_users),
|
||||
assign_change_groups: new FormControl(action.assign_change_groups),
|
||||
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
||||
assign_custom_fields_values: new FormControl(
|
||||
action.assign_custom_fields_values
|
||||
),
|
||||
remove_tags: new FormControl(action.remove_tags),
|
||||
remove_all_tags: new FormControl(action.remove_all_tags),
|
||||
remove_document_types: new FormControl(action.remove_document_types),
|
||||
@@ -565,6 +570,7 @@ export class WorkflowEditDialogComponent
|
||||
assign_change_users: [],
|
||||
assign_change_groups: [],
|
||||
assign_custom_fields: [],
|
||||
assign_custom_fields_values: {},
|
||||
remove_tags: [],
|
||||
remove_all_tags: false,
|
||||
remove_document_types: [],
|
||||
@@ -643,4 +649,12 @@ export class WorkflowEditDialogComponent
|
||||
})
|
||||
super.save()
|
||||
}
|
||||
|
||||
public removeSelectedCustomField(fieldId: number, group: FormGroup) {
|
||||
group
|
||||
.get('assign_custom_fields')
|
||||
.setValue(
|
||||
group.get('assign_custom_fields').value.filter((id) => id !== fieldId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -62,6 +62,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
this.emailAddress = ''
|
||||
this.emailSubject = ''
|
||||
this.emailMessage = ''
|
||||
this.close()
|
||||
this.toastService.showInfo($localize`Email sent`)
|
||||
},
|
||||
error: (e) => {
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
@@ -44,6 +45,11 @@ const nullItem = {
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
const negativeNullItem = {
|
||||
id: NEGATIVE_NULL_FILTER_VALUE,
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
let selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||
@@ -64,6 +70,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
hotkeyService = TestBed.inject(HotKeyService)
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.selectionModel = new FilterableDropdownSelectionModel()
|
||||
selectionModel = new FilterableDropdownSelectionModel()
|
||||
})
|
||||
|
||||
@@ -74,7 +81,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(1)
|
||||
@@ -96,7 +103,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items selected', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -110,11 +117,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
|
||||
expect(component.items).toEqual([nullItem, ...items])
|
||||
expect(component.selectionModel.items).toEqual([nullItem, ...items])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -124,7 +131,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -139,8 +146,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should exclude items when excluded and not editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
component.excludeClicked(items[0].id)
|
||||
@@ -149,8 +156,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should toggle when items excluded and editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
@@ -160,8 +167,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should hide count for item if adding will increase size of set', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.hideCount(items[0])).toBeFalsy()
|
||||
selectionModel.logicalOperator = LogicalOperator.Or
|
||||
@@ -170,7 +177,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should enforce single select when editing', () => {
|
||||
component.editing = true
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
@@ -182,11 +189,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support manyToOne selecting', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
selectionModel.manyToOne = false
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
expect(component.manyToOne).toBeTruthy()
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.selectionModel.manyToOne).toBeTruthy()
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
@@ -197,12 +204,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should dynamically enable / disable modifier toggle', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
selectionModel.toggle(null)
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
@@ -210,7 +215,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
@@ -232,7 +237,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
@@ -250,7 +255,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -277,7 +282,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
@@ -297,7 +302,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
@@ -319,7 +324,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -364,7 +369,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -400,7 +405,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -425,9 +430,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
@@ -454,7 +459,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
@@ -483,22 +488,53 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
expect(changedResult.getExcludedItems()).toEqual(items)
|
||||
}))
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.items = items.concat([{ id: null, name: 'Null B' }])
|
||||
it('should update null item selection on toggleIntersection', () => {
|
||||
component.selectionModel.items = items
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.set(null, ToggleableItemState.Selected)
|
||||
component.selectionModel.intersection = Intersection.Exclude
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getExcludedItems()).toEqual([
|
||||
negativeNullItem,
|
||||
])
|
||||
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel.toggleIntersection()
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
|
||||
})
|
||||
|
||||
it('selection model should sort items by state', () => {
|
||||
component.selectionModel = selectionModel
|
||||
component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
|
||||
selectionModel.toggle(items[1].id)
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items.length).toEqual(4)
|
||||
expect(selectionModel.items).toEqual([
|
||||
nullItem,
|
||||
{ id: null, name: 'Null B' },
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
selectionModel.intersection = Intersection.Exclude
|
||||
selectionModel.toggleIntersection()
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.items).toEqual([
|
||||
negativeNullItem,
|
||||
items[1],
|
||||
{ id: 3, name: 'Item3' },
|
||||
items[0],
|
||||
])
|
||||
|
||||
// coverage
|
||||
selectionModel.items = selectionModel.items.reverse()
|
||||
selectionModel.apply()
|
||||
})
|
||||
|
||||
it('selection model should sort items by state and document counts = 0, if set', () => {
|
||||
const tagA = { id: 4, name: 'Tag A' }
|
||||
component.items = items.concat([tagA])
|
||||
component.selectionModel.items = items.concat([tagA])
|
||||
component.selectionModel = selectionModel
|
||||
component.documentCounts = [
|
||||
{ id: 1, document_count: 0 }, // Tag1
|
||||
@@ -529,7 +565,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
@@ -549,7 +585,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
}))
|
||||
|
||||
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.createRef = jest.fn()
|
||||
@@ -569,7 +605,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
const id = 1
|
||||
const state = ToggleableItemState.Selected
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
component.selectionModel.manyToOne = true
|
||||
component.selectionModel.singleSelect = true
|
||||
component.selectionModel.intersection = Intersection.Include
|
||||
component.selectionModel['temporarySelectionStates'].set(id, state)
|
||||
@@ -596,7 +632,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support shortcut keys', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.shortcutKey = 't'
|
||||
fixture.detectChanges()
|
||||
@@ -606,7 +642,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
})
|
||||
|
||||
it('should support an extra button and not apply changes when clicked', () => {
|
||||
component.items = items
|
||||
component.selectionModel.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.extraButtonTitle = 'Extra'
|
||||
component.selectionModel = selectionModel
|
||||
|
@@ -12,12 +12,13 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, filter, takeUntil } from 'rxjs'
|
||||
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
import { SelectionDataItem } from 'src/app/services/rest/document.service'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
import { pngxPopperOptions } from 'src/app/utils/popper-options'
|
||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import {
|
||||
@@ -61,15 +62,56 @@ export class FilterableDropdownSelectionModel {
|
||||
}
|
||||
|
||||
set items(items: MatchingModel[]) {
|
||||
this._items = items
|
||||
this.sortItems()
|
||||
if (items) {
|
||||
this._items = Array.from(items)
|
||||
this.sortItems()
|
||||
this.setNullItem()
|
||||
}
|
||||
}
|
||||
|
||||
private setNullItem() {
|
||||
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
||||
if (this._items[0]?.id === null) {
|
||||
this._items.shift()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const item = {
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id:
|
||||
this.manyToOne || this.intersection === Intersection.Include
|
||||
? null
|
||||
: NEGATIVE_NULL_FILTER_VALUE,
|
||||
}
|
||||
|
||||
if (
|
||||
this._items[0]?.id === null ||
|
||||
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
||||
) {
|
||||
this._items[0] = item
|
||||
} else if (this._items) {
|
||||
this._items.unshift(item)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(manyToOne: boolean = false) {
|
||||
this.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
private sortItems() {
|
||||
this._items.sort((a, b) => {
|
||||
if (a.id == null && b.id != null) {
|
||||
if (
|
||||
(a.id == null && b.id != null) ||
|
||||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id != NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return -1
|
||||
} else if (a.id != null && b.id == null) {
|
||||
} else if (
|
||||
(a.id != null && b.id == null) ||
|
||||
(a.id != NEGATIVE_NULL_FILTER_VALUE &&
|
||||
b.id == NEGATIVE_NULL_FILTER_VALUE)
|
||||
) {
|
||||
return 1
|
||||
} else if (
|
||||
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||
@@ -230,6 +272,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set logicalOperator(operator: LogicalOperator) {
|
||||
this.temporaryLogicalOperator = operator
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleOperator() {
|
||||
@@ -242,6 +285,7 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
set intersection(intersection: Intersection) {
|
||||
this.temporaryIntersection = intersection
|
||||
this.setNullItem()
|
||||
}
|
||||
|
||||
toggleIntersection() {
|
||||
@@ -250,9 +294,20 @@ export class FilterableDropdownSelectionModel {
|
||||
this.intersection == Intersection.Include
|
||||
? ToggleableItemState.Selected
|
||||
: ToggleableItemState.Excluded
|
||||
|
||||
this.temporarySelectionStates.forEach((state, key) => {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
if (key === null && this.intersection === Intersection.Exclude) {
|
||||
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
||||
} else if (
|
||||
key === NEGATIVE_NULL_FILTER_VALUE &&
|
||||
this.intersection === Intersection.Include
|
||||
) {
|
||||
this.temporarySelectionStates.set(null, newState)
|
||||
} else {
|
||||
this.temporarySelectionStates.set(key, newState)
|
||||
}
|
||||
})
|
||||
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
@@ -274,6 +329,7 @@ export class FilterableDropdownSelectionModel {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||
this.setNullItem()
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
@@ -305,8 +361,10 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
isNoneSelected() {
|
||||
return (
|
||||
this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected
|
||||
(this.selectionSize() == 1 &&
|
||||
this.get(null) == ToggleableItemState.Selected) ||
|
||||
(this.intersection == Intersection.Exclude &&
|
||||
this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -380,29 +438,17 @@ export class FilterableDropdownComponent
|
||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||
@ViewChild('buttonItems') buttonItems: ElementRef
|
||||
|
||||
public popperOptions = popperOptionsReenablePreventOverflow
|
||||
public popperOptions = pngxPopperOptions
|
||||
|
||||
filterText: string
|
||||
|
||||
@Input()
|
||||
set items(items: MatchingModel[]) {
|
||||
if (items) {
|
||||
this._selectionModel.items = Array.from(items)
|
||||
this._selectionModel.items.unshift({
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
_selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
get items(): MatchingModel[] {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel: FilterableDropdownSelectionModel =
|
||||
new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
@@ -423,11 +469,6 @@ export class FilterableDropdownComponent
|
||||
@Output()
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set manyToOne(manyToOne: boolean) {
|
||||
this.selectionModel.manyToOne = manyToOne
|
||||
}
|
||||
|
||||
get manyToOne() {
|
||||
return this.selectionModel.manyToOne
|
||||
}
|
||||
@@ -484,7 +525,7 @@ export class FilterableDropdownComponent
|
||||
return this.manyToOne
|
||||
? this.selectionModel.selectionSize() > 1 &&
|
||||
this.selectionModel.getExcludedItems().length == 0
|
||||
: !this.selectionModel.isNoneSelected()
|
||||
: true
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
|
@@ -0,0 +1,77 @@
|
||||
<div class="list-group mt-3 selected-fields">
|
||||
@for (fieldId of selectedFields; track fieldId) {
|
||||
<div class="list-group-item
|
||||
d-flex
|
||||
justify-content-between
|
||||
align-items-center">
|
||||
@switch (getCustomField(fieldId)?.data_type) {
|
||||
@case (CustomFieldDataType.String) {
|
||||
<pngx-input-text [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"></pngx-input-text>
|
||||
}
|
||||
@case (CustomFieldDataType.Date) {
|
||||
<pngx-input-date [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"></pngx-input-date>
|
||||
}
|
||||
@case (CustomFieldDataType.Integer) {
|
||||
<pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"></pngx-input-number>
|
||||
}
|
||||
@case (CustomFieldDataType.Float) {
|
||||
<pngx-input-number [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".1"></pngx-input-number>
|
||||
}
|
||||
@case (CustomFieldDataType.Monetary) {
|
||||
<pngx-input-monetary [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[defaultCurrency]="getCustomField(fieldId)?.extra_data?.default_currency"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"></pngx-input-monetary>
|
||||
}
|
||||
@case (CustomFieldDataType.Boolean) {
|
||||
<pngx-input-check [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"></pngx-input-check>
|
||||
}
|
||||
@case (CustomFieldDataType.Url) {
|
||||
<pngx-input-url [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"></pngx-input-url>
|
||||
}
|
||||
@case (CustomFieldDataType.DocumentLink) {
|
||||
<pngx-input-document-link [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[horizontal]="true"></pngx-input-document-link>
|
||||
}
|
||||
@case (CustomFieldDataType.Select) {
|
||||
<pngx-input-select [(ngModel)]="value[fieldId]" (ngModelChange)="onChange(value)"
|
||||
[title]="getCustomField(fieldId)?.name"
|
||||
class="flex-grow-1"
|
||||
[items]="getCustomField(fieldId)?.extra_data.select_options"
|
||||
class="flex-grow-1"
|
||||
bindLabel="label"
|
||||
[allowNull]="true"
|
||||
[horizontal]="true"></pngx-input-select>
|
||||
}
|
||||
}
|
||||
<button type="button" class="btn btn-link text-danger" (click)="removeSelectedField.next(fieldId)">
|
||||
<i-bs name="trash"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
:host ::ng-deep .list-group-item .mb-3 {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { of } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomFieldsValuesComponent } from './custom-fields-values.component'
|
||||
|
||||
describe('CustomFieldsValuesComponent', () => {
|
||||
let component: CustomFieldsValuesComponent
|
||||
let fixture: ComponentFixture<CustomFieldsValuesComponent>
|
||||
let customFieldsService: CustomFieldsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormsModule, ReactiveFormsModule, CustomFieldsValuesComponent],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(CustomFieldsValuesComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: [1],
|
||||
count: 1,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Field 1',
|
||||
data_type: CustomFieldDataType.String,
|
||||
} as CustomField,
|
||||
],
|
||||
})
|
||||
)
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CustomFieldsValuesComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should set selectedFields and map values correctly', () => {
|
||||
component.value = { 1: 'value1' }
|
||||
component.selectedFields = [1, 2]
|
||||
expect(component.selectedFields).toEqual([1, 2])
|
||||
expect(component.value).toEqual({ 1: 'value1', 2: null })
|
||||
})
|
||||
|
||||
it('should return the correct custom field by id', () => {
|
||||
const field = component.getCustomField(1)
|
||||
expect(field).toEqual({
|
||||
id: 1,
|
||||
name: 'Field 1',
|
||||
data_type: CustomFieldDataType.String,
|
||||
} as CustomField)
|
||||
})
|
||||
})
|
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
import { CheckComponent } from '../check/check.component'
|
||||
import { DateComponent } from '../date/date.component'
|
||||
import { DocumentLinkComponent } from '../document-link/document-link.component'
|
||||
import { MonetaryComponent } from '../monetary/monetary.component'
|
||||
import { NumberComponent } from '../number/number.component'
|
||||
import { SelectComponent } from '../select/select.component'
|
||||
import { TextComponent } from '../text/text.component'
|
||||
import { UrlComponent } from '../url/url.component'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => CustomFieldsValuesComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'pngx-input-custom-fields-values',
|
||||
templateUrl: './custom-fields-values.component.html',
|
||||
styleUrl: './custom-fields-values.component.scss',
|
||||
imports: [
|
||||
TextComponent,
|
||||
DateComponent,
|
||||
NumberComponent,
|
||||
DocumentLinkComponent,
|
||||
UrlComponent,
|
||||
SelectComponent,
|
||||
MonetaryComponent,
|
||||
CheckComponent,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> {
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
constructor(customFieldsService: CustomFieldsService) {
|
||||
super()
|
||||
customFieldsService.listAll().subscribe((items) => {
|
||||
this.fields = items.results
|
||||
})
|
||||
}
|
||||
|
||||
private fields: CustomField[]
|
||||
|
||||
private _selectedFields: number[]
|
||||
|
||||
@Input()
|
||||
set selectedFields(newFields: number[]) {
|
||||
this._selectedFields = newFields
|
||||
// map the selected fields to an object with field_id as key and value as value
|
||||
this.value = newFields.reduce((acc, fieldId) => {
|
||||
acc[fieldId] = this.value?.[fieldId] || null
|
||||
return acc
|
||||
}, {})
|
||||
this.onChange(this.value)
|
||||
}
|
||||
|
||||
get selectedFields(): number[] {
|
||||
return this._selectedFields
|
||||
}
|
||||
|
||||
@Output()
|
||||
public removeSelectedField: EventEmitter<number> = new EventEmitter<number>()
|
||||
|
||||
public getCustomField(id: number): CustomField {
|
||||
return this.fields.find((field) => field.id === id)
|
||||
}
|
||||
}
|
@@ -30,25 +30,24 @@
|
||||
[placeholder]="placeholder"
|
||||
[notFoundText]="notFoundText"
|
||||
[multiple]="true"
|
||||
bindValue="id"
|
||||
[compareWith]="compareDocuments"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="loading"
|
||||
[typeahead]="documentsInput$"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
(change)="onChange(selectedDocuments)">
|
||||
(change)="onChange(selectedDocumentIDs)">
|
||||
<ng-template ng-label-tmp let-document="item">
|
||||
<div class="d-flex align-items-center">
|
||||
@if (!disabled) {
|
||||
<button class="btn p-0 lh-1" (click)="unselect(document)" title="Remove link" i18n-title><i-bs name="x"></i-bs></button>
|
||||
<button class="btn p-0 lh-1" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title><i-bs name="x"></i-bs></button>
|
||||
}
|
||||
@if (document.title) {
|
||||
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();" title="Open link" i18n-title>
|
||||
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{document.title}}</span>
|
||||
</a>
|
||||
} @else {
|
||||
<span class="badge bg-light text-muted" (click)="unselect(document)" title="Remove link" i18n-title>
|
||||
<span class="badge bg-light text-muted" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title>
|
||||
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle-fill"></i-bs> <span i18n>Not found</span>
|
||||
</span>
|
||||
}
|
||||
|
@@ -74,6 +74,11 @@ describe('DocumentLinkComponent', () => {
|
||||
expect(component.selectedDocuments).toEqual([documents[1], documents[0]])
|
||||
})
|
||||
|
||||
it('should retrieve document IDs from selected documents', () => {
|
||||
component.selectedDocuments = documents
|
||||
expect(component.selectedDocumentIDs).toEqual([1, 12, 16, 23])
|
||||
})
|
||||
|
||||
it('should search API on select text input', () => {
|
||||
const listSpy = jest.spyOn(documentService, 'listFiltered')
|
||||
listSpy.mockImplementation(
|
||||
|
@@ -71,6 +71,10 @@ export class DocumentLinkComponent
|
||||
@Input()
|
||||
placeholder: string = $localize`Search for documents`
|
||||
|
||||
get selectedDocumentIDs(): number[] {
|
||||
return this.selectedDocuments.map((d) => d.id)
|
||||
}
|
||||
|
||||
constructor(private documentsService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
@@ -90,8 +94,8 @@ export class DocumentLinkComponent
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((documentResults) => {
|
||||
this.loading = false
|
||||
this.selectedDocuments = documentIDs.map((id) =>
|
||||
documentResults.results.find((d) => d.id === id)
|
||||
this.selectedDocuments = documentIDs.map(
|
||||
(id) => documentResults.results.find((d) => d.id === id) ?? {}
|
||||
)
|
||||
super.writeValue(documentIDs)
|
||||
})
|
||||
|
@@ -17,7 +17,7 @@
|
||||
(change)="onChange(value)">
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
|
||||
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag(item.id)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove tag" i18n-title>
|
||||
<i-bs name="x" style="margin-inline-end: 1px;"></i-bs>
|
||||
@if (item.id && tags) {
|
||||
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
|
||||
|
@@ -154,11 +154,11 @@ describe('TagsComponent', () => {
|
||||
it('support remove tags', () => {
|
||||
component.tags = tags
|
||||
component.value = [1, 2]
|
||||
component.removeTag(new PointerEvent('point'), 2)
|
||||
component.removeTag(2)
|
||||
expect(component.value).toEqual([1])
|
||||
|
||||
component.disabled = true
|
||||
component.removeTag(new PointerEvent('point'), 1)
|
||||
component.removeTag(1)
|
||||
expect(component.value).toEqual([1])
|
||||
})
|
||||
|
||||
|
@@ -118,13 +118,10 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(event: PointerEvent, id: number) {
|
||||
removeTag(tagID: number) {
|
||||
if (this.disabled) return
|
||||
|
||||
// prevent opening dropdown
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
let index = this.value.indexOf(id)
|
||||
let index = this.value.indexOf(tagID)
|
||||
if (index > -1) {
|
||||
let oldValue = this.value
|
||||
oldValue.splice(index, 1)
|
||||
|
@@ -1,14 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<pngx-input-select [items]="objects" [title]="message" [(ngModel)]="selected"></pngx-input-select>
|
||||
|
||||
</div>
|
||||
<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>
|
@@ -1,36 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { SelectComponent } from '../input/select/select.component'
|
||||
import { SelectDialogComponent } from './select-dialog.component'
|
||||
|
||||
describe('SelectDialogComponent', () => {
|
||||
let component: SelectDialogComponent
|
||||
let fixture: ComponentFixture<SelectDialogComponent>
|
||||
let modal: NgbActiveModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
SelectDialogComponent,
|
||||
SelectComponent,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
fixture = TestBed.createComponent(SelectDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should close modal on cancel', () => {
|
||||
const closeSpy = jest.spyOn(modal, 'close')
|
||||
component.cancelClicked()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -1,33 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
import { SelectComponent } from '../input/select/select.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-select-dialog',
|
||||
templateUrl: './select-dialog.component.html',
|
||||
styleUrls: ['./select-dialog.component.scss'],
|
||||
imports: [SelectComponent, FormsModule, ReactiveFormsModule],
|
||||
})
|
||||
export class SelectDialogComponent {
|
||||
constructor(public activeModal: NgbActiveModal) {}
|
||||
|
||||
@Output()
|
||||
public selectClicked = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
title = $localize`Select`
|
||||
|
||||
@Input()
|
||||
message = $localize`Please select an object`
|
||||
|
||||
@Input()
|
||||
objects: ObjectWithId[] = []
|
||||
|
||||
selected: number
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user