mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			6a1060f4d2
			...
			f8fb224968
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f8fb224968 | ||
|   | 27d1c76268 | ||
|   | ddfd552e8b | ||
|   | c5e8708242 | ||
|   | b2567f990e | ||
|   | 5389876c91 | ||
|   | ba12a27119 | ||
|   | da8ecfa139 | ||
|   | e93b2c1cad | 
| @@ -76,15 +76,18 @@ RUN set -eux \ | |||||||
|     && apt-get update \ |     && apt-get update \ | ||||||
|     && apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} |     && apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} | ||||||
|  |  | ||||||
| ARG PYTHON_PACKAGES="ca-certificates" | ARG PYTHON_PACKAGES="\ | ||||||
|  |   python3 \ | ||||||
|  |   python3-pip \ | ||||||
|  |   python3-wheel \ | ||||||
|  |   pipenv \ | ||||||
|  |   ca-certificates" | ||||||
|  |  | ||||||
| RUN set -eux \ | RUN set -eux \ | ||||||
|   echo "Installing python packages" \ |   echo "Installing python packages" \ | ||||||
|     && apt-get update \ |     && apt-get update \ | ||||||
|     && apt-get install --yes --quiet ${PYTHON_PACKAGES} |     && apt-get install --yes --quiet ${PYTHON_PACKAGES} | ||||||
|  |  | ||||||
| COPY --from=ghcr.io/astral-sh/uv:0.6 /uv /bin/uv |  | ||||||
|  |  | ||||||
| RUN set -eux \ | RUN set -eux \ | ||||||
|   && echo "Installing pre-built updates" \ |   && echo "Installing pre-built updates" \ | ||||||
|     && echo "Installing qpdf ${QPDF_VERSION}" \ |     && echo "Installing qpdf ${QPDF_VERSION}" \ | ||||||
| @@ -128,8 +131,6 @@ RUN set -eux \ | |||||||
|   && echo "Configuring ImageMagick" \ |   && echo "Configuring ImageMagick" \ | ||||||
|     && mv paperless-policy.xml /etc/ImageMagick-6/policy.xml |     && 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 | # Packages needed only for building a few quick Python | ||||||
| # dependencies | # dependencies | ||||||
| ARG BUILD_PACKAGES="\ | ARG BUILD_PACKAGES="\ | ||||||
| @@ -139,10 +140,11 @@ ARG BUILD_PACKAGES="\ | |||||||
|   libpq-dev \ |   libpq-dev \ | ||||||
|   # https://github.com/PyMySQL/mysqlclient#linux |   # https://github.com/PyMySQL/mysqlclient#linux | ||||||
|   default-libmysqlclient-dev \ |   default-libmysqlclient-dev \ | ||||||
|   pkg-config" |   pkg-config \ | ||||||
|  |   pre-commit" | ||||||
|  |  | ||||||
| # hadolint ignore=DL3042 | # hadolint ignore=DL3042 | ||||||
| RUN --mount=type=cache,target=/root/.cache/uv,id=pip-cache \ | RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \ | ||||||
|   set -eux \ |   set -eux \ | ||||||
|   && echo "Installing build system packages" \ |   && echo "Installing build system packages" \ | ||||||
|     && apt-get update \ |     && apt-get update \ | ||||||
| @@ -167,6 +169,9 @@ RUN set -eux \ | |||||||
|     && mkdir --parents --verbose /usr/src/paperless/paperless-ngx/.venv \ |     && mkdir --parents --verbose /usr/src/paperless/paperless-ngx/.venv \ | ||||||
|   && echo "Adjusting all permissions" \ |   && echo "Adjusting all permissions" \ | ||||||
|     && chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless |     && 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", \ | VOLUME ["/usr/src/paperless/paperless-ngx/data", \ | ||||||
|         "/usr/src/paperless/paperless-ngx/media", \ |         "/usr/src/paperless/paperless-ngx/media", \ | ||||||
|   | |||||||
| @@ -1,117 +0,0 @@ | |||||||
| # 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", |     "dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml", | ||||||
|     "service": "paperless-development", |     "service": "paperless-development", | ||||||
|     "workspaceFolder": "/usr/src/paperless/paperless-ngx", |     "workspaceFolder": "/usr/src/paperless/paperless-ngx", | ||||||
|     "postCreateCommand": "/bin/bash -c uv sync --dev && uv run pre-commit install", |     "postCreateCommand": "pipenv install --dev && pipenv run pre-commit install", | ||||||
|     "customizations": { |     "customizations": { | ||||||
|         "vscode": { |         "vscode": { | ||||||
|           "extensions": [ |           "extensions": [ | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - ..:/usr/src/paperless/paperless-ngx:delegated |       - ..:/usr/src/paperless/paperless-ngx:delegated | ||||||
|       - ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files |       - ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files | ||||||
|       - virtualenv:/usr/src/paperless/paperless-ngx/.venv # Virtual environment persisted in volume |       - pipenv:/usr/src/paperless/paperless-ngx/.venv | ||||||
|       - /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container |       - /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/src/.pytest_cache | ||||||
|       - /usr/src/paperless/paperless-ngx/.ruff_cache |       - /usr/src/paperless/paperless-ngx/.ruff_cache | ||||||
| @@ -80,7 +80,4 @@ services: | |||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|  |  | ||||||
| volumes: | volumes: | ||||||
|   data: |   pipenv: | ||||||
|   media: |  | ||||||
|   redisdata: |  | ||||||
|   virtualenv: |  | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| 			"label": "Start: Celery Worker", | 			"label": "Start: Celery Worker", | ||||||
| 			"description": "Start the Celery Worker which processes background and consume tasks", | 			"description": "Start the Celery Worker which processes background and consume tasks", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "uv run celery --app paperless worker -l DEBUG", | 			"command": "pipenv run celery --app paperless worker -l DEBUG", | ||||||
| 			"isBackground": true, | 			"isBackground": true, | ||||||
| 			"options": { | 			"options": { | ||||||
| 				"cwd": "${workspaceFolder}/src" | 				"cwd": "${workspaceFolder}/src" | ||||||
| @@ -61,7 +61,7 @@ | |||||||
| 			"label": "Start: Consumer Service (manage.py document_consumer)", | 			"label": "Start: Consumer Service (manage.py document_consumer)", | ||||||
| 			"description": "Start the Consumer Service which processes files from a directory", | 			"description": "Start the Consumer Service which processes files from a directory", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "uv run python manage.py document_consumer", | 			"command": "pipenv run python manage.py document_consumer", | ||||||
| 			"group": "build", | 			"group": "build", | ||||||
| 			"presentation": { | 			"presentation": { | ||||||
| 				"echo": true, | 				"echo": true, | ||||||
| @@ -80,7 +80,7 @@ | |||||||
| 			"label": "Start: Backend Server (manage.py runserver)", | 			"label": "Start: Backend Server (manage.py runserver)", | ||||||
| 			"description": "Start the Backend Server which serves the Django API and the compiled Angular frontend", | 			"description": "Start the Backend Server which serves the Django API and the compiled Angular frontend", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "uv run python manage.py runserver", | 			"command": "pipenv run python manage.py runserver", | ||||||
| 			"group": "build", | 			"group": "build", | ||||||
| 			"presentation": { | 			"presentation": { | ||||||
| 				"echo": true, | 				"echo": true, | ||||||
| @@ -99,7 +99,7 @@ | |||||||
| 			"label": "Maintenance: manage.py migrate", | 			"label": "Maintenance: manage.py migrate", | ||||||
| 			"description": "Apply database migrations", | 			"description": "Apply database migrations", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "uv run python manage.py migrate", | 			"command": "pipenv run python manage.py migrate", | ||||||
| 			"group": "none", | 			"group": "none", | ||||||
| 			"presentation": { | 			"presentation": { | ||||||
| 				"echo": true, | 				"echo": true, | ||||||
| @@ -118,7 +118,7 @@ | |||||||
| 			"label": "Maintenance: Build Documentation", | 			"label": "Maintenance: Build Documentation", | ||||||
| 			"description": "Build the documentation with MkDocs", | 			"description": "Build the documentation with MkDocs", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "uv run mkdocs build --config-file mkdocs.yml && uv run mkdocs serve", | 			"command": "pipenv run mkdocs build --config-file mkdocs.yml && pipenv run mkdocs serve", | ||||||
| 			"group": "none", | 			"group": "none", | ||||||
| 			"presentation": { | 			"presentation": { | ||||||
| 				"echo": true, | 				"echo": true, | ||||||
| @@ -137,7 +137,7 @@ | |||||||
| 			"label": "Maintenance: manage.py createsuperuser", | 			"label": "Maintenance: manage.py createsuperuser", | ||||||
| 			"description": "Create a superuser", | 			"description": "Create a superuser", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "uv run python manage.py createsuperuser", | 			"command": "pipenv run python manage.py createsuperuser", | ||||||
| 			"group": "none", | 			"group": "none", | ||||||
| 			"presentation": { | 			"presentation": { | ||||||
| 				"echo": true, | 				"echo": true, | ||||||
| @@ -156,7 +156,7 @@ | |||||||
| 			"label": "Maintenance: recreate .venv", | 			"label": "Maintenance: recreate .venv", | ||||||
| 			"description": "Recreate the python virtual environment and install python dependencies", | 			"description": "Recreate the python virtual environment and install python dependencies", | ||||||
| 			"type": "shell", | 			"type": "shell", | ||||||
| 			"command": "rm -R -v .venv/* || uv install --dev", | 			"command": "rm -R -v .venv/* || pipenv install --dev", | ||||||
| 			"group": "none", | 			"group": "none", | ||||||
| 			"presentation": { | 			"presentation": { | ||||||
| 				"echo": true, | 				"echo": true, | ||||||
|   | |||||||
| @@ -27,6 +27,9 @@ indent_style = space | |||||||
| [*.md] | [*.md] | ||||||
| indent_style = space | indent_style = space | ||||||
|  |  | ||||||
|  | [Pipfile.lock] | ||||||
|  | indent_style = space | ||||||
|  |  | ||||||
| # Tests don't get a line width restriction.  It's still a good idea to follow | # 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 | # the 79 character rule, but in the interests of clarity, tests often need to | ||||||
| # violate it. | # violate it. | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,6 @@ | |||||||
| # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem | ||||||
|  |  | ||||||
| version: 2 | version: 2 | ||||||
| # Required for uv support for now |  | ||||||
| enable-beta-ecosystems: true |  | ||||||
| updates: | updates: | ||||||
|  |  | ||||||
|   # Enable version updates for npm |   # Enable version updates for npm | ||||||
| @@ -36,8 +34,9 @@ updates: | |||||||
|           - "eslint" |           - "eslint" | ||||||
|  |  | ||||||
|   # Enable version updates for Python |   # Enable version updates for Python | ||||||
|   - package-ecosystem: "uv" |   - package-ecosystem: "pip" | ||||||
|     target-branch: "dev" |     target-branch: "dev" | ||||||
|  |     # Look for a `Pipfile` in the `root` directory | ||||||
|     directory: "/" |     directory: "/" | ||||||
|     # Check for updates once a week |     # Check for updates once a week | ||||||
|     schedule: |     schedule: | ||||||
| @@ -54,7 +53,6 @@ updates: | |||||||
|           - "*pytest*" |           - "*pytest*" | ||||||
|           - "ruff" |           - "ruff" | ||||||
|           - "mkdocs-material" |           - "mkdocs-material" | ||||||
|           - "pre-commit*" |  | ||||||
|       django: |       django: | ||||||
|         patterns: |         patterns: | ||||||
|           - "*django*" |           - "*django*" | ||||||
| @@ -65,10 +63,6 @@ updates: | |||||||
|         update-types: |         update-types: | ||||||
|           - "minor" |           - "minor" | ||||||
|           - "patch" |           - "patch" | ||||||
|       pre-built: |  | ||||||
|         patterns: |  | ||||||
|           - psycopg* |  | ||||||
|           - zxing-cpp |  | ||||||
|  |  | ||||||
|   # Enable updates for GitHub Actions |   # Enable updates for GitHub Actions | ||||||
|   - package-ecosystem: "github-actions" |   - package-ecosystem: "github-actions" | ||||||
|   | |||||||
							
								
								
									
										150
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										150
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,9 @@ on: | |||||||
|       - 'translations**' |       - 'translations**' | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   DEFAULT_UV_VERSION: "0.6.x" |   # This is the version of pipenv all the steps will use | ||||||
|  |   # If changing this, change Dockerfile | ||||||
|  |   DEFAULT_PIP_ENV_VERSION: "2024.4.1" | ||||||
|   # This is the default version of Python to use in most steps which aren't specific |   # This is the default version of Python to use in most steps which aren't specific | ||||||
|   DEFAULT_PYTHON_VERSION: "3.11" |   DEFAULT_PYTHON_VERSION: "3.11" | ||||||
|  |  | ||||||
| @@ -57,25 +59,24 @@ jobs: | |||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} |           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | ||||||
|  |           cache: "pipenv" | ||||||
|  |           cache-dependency-path: 'Pipfile.lock' | ||||||
|       - |       - | ||||||
|         name: Install uv |         name: Install pipenv | ||||||
|         uses: astral-sh/setup-uv@v5 |  | ||||||
|         with: |  | ||||||
|           version: ${{ env.DEFAULT_UV_VERSION }} |  | ||||||
|           enable-cache: true |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} |  | ||||||
|       - |  | ||||||
|         name: Install Python dependencies |  | ||||||
|         run: | |         run: | | ||||||
|           uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen |           pip install --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} | ||||||
|  |       - | ||||||
|  |         name: Install 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 | ||||||
|       - |       - | ||||||
|         name: Make documentation |         name: Make documentation | ||||||
|         run: | |         run: | | ||||||
|           uv run \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs build --config-file ./mkdocs.yml | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             --dev \ |  | ||||||
|             --frozen \ |  | ||||||
|             mkdocs build --config-file ./mkdocs.yml |  | ||||||
|       - |       - | ||||||
|         name: Deploy documentation |         name: Deploy documentation | ||||||
|         if: github.event_name == 'push' && github.ref == 'refs/heads/main' |         if: github.event_name == 'push' && github.ref == 'refs/heads/main' | ||||||
| @@ -83,11 +84,7 @@ jobs: | |||||||
|           echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME" |           echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME" | ||||||
|           git config --global user.name "${{ github.actor }}" |           git config --global user.name "${{ github.actor }}" | ||||||
|           git config --global user.email "${{ github.actor }}@users.noreply.github.com" |           git config --global user.email "${{ github.actor }}@users.noreply.github.com" | ||||||
|           uv run \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             --dev \ |  | ||||||
|             --frozen \ |  | ||||||
|             mkdocs gh-deploy --force --no-history |  | ||||||
|       - |       - | ||||||
|         name: Upload artifact |         name: Upload artifact | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v4 | ||||||
| @@ -120,13 +117,12 @@ jobs: | |||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: "${{ matrix.python-version }}" |           python-version: "${{ matrix.python-version }}" | ||||||
|  |           cache: "pipenv" | ||||||
|  |           cache-dependency-path: 'Pipfile.lock' | ||||||
|       - |       - | ||||||
|         name: Install uv |         name: Install pipenv | ||||||
|         uses: astral-sh/setup-uv@v5 |         run: | | ||||||
|         with: |           pip install --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} | ||||||
|           version: ${{ env.DEFAULT_UV_VERSION }} |  | ||||||
|           enable-cache: true |  | ||||||
|           python-version: ${{ steps.setup-python.outputs.python-version }} |  | ||||||
|       - |       - | ||||||
|         name: Install system dependencies |         name: Install system dependencies | ||||||
|         run: | |         run: | | ||||||
| @@ -139,14 +135,12 @@ jobs: | |||||||
|       - |       - | ||||||
|         name: Install Python dependencies |         name: Install Python dependencies | ||||||
|         run: | |         run: | | ||||||
|           uv sync \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run python --version | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev | ||||||
|             --group testing \ |  | ||||||
|             --frozen |  | ||||||
|       - |       - | ||||||
|         name: List installed Python dependencies |         name: List installed Python dependencies | ||||||
|         run: | |         run: | | ||||||
|           uv pip list |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run pip list | ||||||
|       - |       - | ||||||
|         name: Tests |         name: Tests | ||||||
|         env: |         env: | ||||||
| @@ -156,22 +150,17 @@ jobs: | |||||||
|           PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }} |           PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }} | ||||||
|           PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }} |           PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }} | ||||||
|         run: | |         run: | | ||||||
|           uv run \ |           cd src/ | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run pytest -ra | ||||||
|             --dev \ |  | ||||||
|             --frozen \ |  | ||||||
|             pytest |  | ||||||
|       - |       - | ||||||
|         name: Upload coverage |         name: Upload coverage | ||||||
|         if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }} |         if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }} | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           name: backend-coverage-report |           name: backend-coverage-report | ||||||
|           path: | |           path: src/coverage.xml | ||||||
|             coverage.xml |  | ||||||
|             junit.xml |  | ||||||
|           retention-days: 7 |           retention-days: 7 | ||||||
|           if-no-files-found: error |           if-no-files-found: warn | ||||||
|       - |       - | ||||||
|         name: Stop containers |         name: Stop containers | ||||||
|         if: always() |         if: always() | ||||||
| @@ -245,8 +234,6 @@ jobs: | |||||||
|         run: cd src-ui && npm run lint |         run: cd src-ui && npm run lint | ||||||
|       - |       - | ||||||
|         name: Run Jest unit tests |         name: Run Jest unit tests | ||||||
|         env: |  | ||||||
|           JEST_JUNIT_OUTPUT_FILE: junit-report-${{ matrix.shard-index }}.xml |  | ||||||
|         run: cd src-ui && npm run test -- --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }} |         run: cd src-ui && npm run test -- --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }} | ||||||
|       - |       - | ||||||
|         name: Upload Jest coverage |         name: Upload Jest coverage | ||||||
| @@ -259,7 +246,7 @@ jobs: | |||||||
|             src-ui/coverage/lcov.info |             src-ui/coverage/lcov.info | ||||||
|             src-ui/coverage/clover.xml |             src-ui/coverage/clover.xml | ||||||
|           retention-days: 7 |           retention-days: 7 | ||||||
|           if-no-files-found: error |           if-no-files-found: warn | ||||||
|       - |       - | ||||||
|         name: Run Playwright e2e tests |         name: Run Playwright e2e tests | ||||||
|         run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }} |         run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }} | ||||||
| @@ -271,16 +258,6 @@ jobs: | |||||||
|           name: playwright-report-${{ matrix.shard-index }} |           name: playwright-report-${{ matrix.shard-index }} | ||||||
|           path: src-ui/playwright-report |           path: src-ui/playwright-report | ||||||
|           retention-days: 7 |           retention-days: 7 | ||||||
|           if-no-files-found: error |  | ||||||
|       - |  | ||||||
|         name: Upload frontend test results |  | ||||||
|         if: always() |  | ||||||
|         uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: junit-report-${{ matrix.shard-index }} |  | ||||||
|           path: src-ui/junit-report-${{ matrix.shard-index }}.xml |  | ||||||
|           retention-days: 7 |  | ||||||
|           if-no-files-found: error |  | ||||||
|  |  | ||||||
|   tests-coverage-upload: |   tests-coverage-upload: | ||||||
|     name: "Upload to Codecov" |     name: "Upload to Codecov" | ||||||
| @@ -304,13 +281,6 @@ jobs: | |||||||
|           path: src-ui/coverage/ |           path: src-ui/coverage/ | ||||||
|           pattern: playwright-report-* |           pattern: playwright-report-* | ||||||
|           merge-multiple: true |           merge-multiple: true | ||||||
|       - |  | ||||||
|         name: Download frontend test results |  | ||||||
|         uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           path: src-ui/junit/ |  | ||||||
|           pattern: junit-report-* |  | ||||||
|           merge-multiple: true |  | ||||||
|       - |       - | ||||||
|         name: Upload frontend coverage to Codecov |         name: Upload frontend coverage to Codecov | ||||||
|         uses: codecov/codecov-action@v5 |         uses: codecov/codecov-action@v5 | ||||||
| @@ -321,14 +291,6 @@ jobs: | |||||||
|           directory: src-ui/coverage/ |           directory: src-ui/coverage/ | ||||||
|           # dont include backend coverage files here |           # dont include backend coverage files here | ||||||
|           files: '!coverage.xml' |           files: '!coverage.xml' | ||||||
|       - |  | ||||||
|         name: Upload frontend test results to Codecov |  | ||||||
|         if: ${{ !cancelled() }} |  | ||||||
|         uses: codecov/test-results-action@v1 |  | ||||||
|         with: |  | ||||||
|           token: ${{ secrets.CODECOV_TOKEN }} |  | ||||||
|           flags: frontend |  | ||||||
|           directory: src-ui/junit/ |  | ||||||
|       - |       - | ||||||
|         name: Download backend coverage |         name: Download backend coverage | ||||||
|         uses: actions/download-artifact@v4 |         uses: actions/download-artifact@v4 | ||||||
| @@ -344,14 +306,6 @@ jobs: | |||||||
|           # future expansion |           # future expansion | ||||||
|           flags: backend |           flags: backend | ||||||
|           directory: src/ |           directory: src/ | ||||||
|       - |  | ||||||
|         name: Upload backend test results to Codecov |  | ||||||
|         if: ${{ !cancelled() }} |  | ||||||
|         uses: codecov/test-results-action@v1 |  | ||||||
|         with: |  | ||||||
|           token: ${{ secrets.CODECOV_TOKEN }} |  | ||||||
|           flags: backend |  | ||||||
|           directory: src/ |  | ||||||
|       - |       - | ||||||
|         name: Use Node.js 20 |         name: Use Node.js 20 | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v4 | ||||||
| @@ -518,17 +472,16 @@ jobs: | |||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} |           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | ||||||
|  |           cache: "pipenv" | ||||||
|  |           cache-dependency-path: 'Pipfile.lock' | ||||||
|       - |       - | ||||||
|         name: Install uv |         name: Install pipenv + tools | ||||||
|         uses: astral-sh/setup-uv@v5 |         run: | | ||||||
|         with: |           pip install --upgrade --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} setuptools wheel | ||||||
|           version: ${{ env.DEFAULT_UV_VERSION }} |  | ||||||
|           enable-cache: true |  | ||||||
|           python-version: ${{ steps.setup-python.outputs.python-version }} |  | ||||||
|       - |       - | ||||||
|         name: Install Python dependencies |         name: Install Python dependencies | ||||||
|         run: | |         run: | | ||||||
|           uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen |           pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev | ||||||
|       - |       - | ||||||
|         name: Install system dependencies |         name: Install system dependencies | ||||||
|         run: | |         run: | | ||||||
| @@ -549,21 +502,17 @@ jobs: | |||||||
|       - |       - | ||||||
|         name: Generate requirements file |         name: Generate requirements file | ||||||
|         run: | |         run: | | ||||||
|            uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt |           pipenv --python ${{ steps.setup-python.outputs.python-version }} requirements > requirements.txt | ||||||
|       - |       - | ||||||
|         name: Compile messages |         name: Compile messages | ||||||
|         run: | |         run: | | ||||||
|           cd src/ |           cd src/ | ||||||
|           uv run \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run python3 manage.py compilemessages | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             manage.py compilemessages |  | ||||||
|       - |       - | ||||||
|         name: Collect static files |         name: Collect static files | ||||||
|         run: | |         run: | | ||||||
|           cd src/ |           cd src/ | ||||||
|           uv run \ |           pipenv --python ${{ steps.setup-python.outputs.python-version }} run python3 manage.py collectstatic --no-input | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             manage.py collectstatic --no-input |  | ||||||
|       - |       - | ||||||
|         name: Move files |         name: Move files | ||||||
|         run: | |         run: | | ||||||
| @@ -579,8 +528,8 @@ jobs: | |||||||
|           for file_name in .dockerignore \ |           for file_name in .dockerignore \ | ||||||
|                           .env \ |                           .env \ | ||||||
|                           Dockerfile \ |                           Dockerfile \ | ||||||
|                           pyproject.toml \ |                           Pipfile \ | ||||||
|                           uv.lock \ |                           Pipfile.lock \ | ||||||
|                           requirements.txt \ |                           requirements.txt \ | ||||||
|                           LICENSE \ |                           LICENSE \ | ||||||
|                           README.md \ |                           README.md \ | ||||||
| @@ -682,17 +631,15 @@ jobs: | |||||||
|           ref: main |           ref: main | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         id: setup-python |  | ||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} |           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | ||||||
|  |           cache: "pipenv" | ||||||
|  |           cache-dependency-path: 'Pipfile.lock' | ||||||
|       - |       - | ||||||
|         name: Install uv |         name: Install pipenv + tools | ||||||
|         uses: astral-sh/setup-uv@v5 |         run: | | ||||||
|         with: |           pip install --upgrade --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} setuptools wheel | ||||||
|           version: ${{ env.DEFAULT_UV_VERSION }} |  | ||||||
|           enable-cache: true |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} |  | ||||||
|       - |       - | ||||||
|         name: Append Changelog to docs |         name: Append Changelog to docs | ||||||
|         id: append-Changelog |         id: append-Changelog | ||||||
| @@ -708,10 +655,7 @@ jobs: | |||||||
|           CURRENT_CHANGELOG=`tail --lines +2 changelog.md` |           CURRENT_CHANGELOG=`tail --lines +2 changelog.md` | ||||||
|           echo -e "$CURRENT_CHANGELOG" >> changelog-new.md |           echo -e "$CURRENT_CHANGELOG" >> changelog-new.md | ||||||
|           mv changelog-new.md changelog.md |           mv changelog-new.md changelog.md | ||||||
|           uv run \ |           pipenv run pre-commit run --files changelog.md || true | ||||||
|             --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.name "github-actions" | ||||||
|           git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" |           git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||||||
|           git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA" |           git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA" | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -44,7 +44,6 @@ nosetests.xml | |||||||
| coverage.xml | coverage.xml | ||||||
| *,cover | *,cover | ||||||
| .pytest_cache | .pytest_cache | ||||||
| junit.xml |  | ||||||
|  |  | ||||||
| # Translations | # Translations | ||||||
| *.mo | *.mo | ||||||
|   | |||||||
| @@ -45,6 +45,7 @@ repos: | |||||||
|           - javascript |           - javascript | ||||||
|           - ts |           - ts | ||||||
|           - markdown |           - markdown | ||||||
|  |         exclude: "(^Pipfile\\.lock$)" | ||||||
|         additional_dependencies: |         additional_dependencies: | ||||||
|           - prettier@3.3.3 |           - prettier@3.3.3 | ||||||
|           - 'prettier-plugin-organize-imports@4.1.0' |           - 'prettier-plugin-organize-imports@4.1.0' | ||||||
| @@ -54,10 +55,6 @@ repos: | |||||||
|     hooks: |     hooks: | ||||||
|       - id: ruff |       - id: ruff | ||||||
|       - id: ruff-format |       - id: ruff-format | ||||||
|   - repo: https://github.com/tox-dev/pyproject-fmt |  | ||||||
|     rev: "v2.5.1" |  | ||||||
|     hooks: |  | ||||||
|       - id: pyproject-fmt |  | ||||||
|   # Dockerfile hooks |   # Dockerfile hooks | ||||||
|   - repo: https://github.com/AleksaC/hadolint-py |   - repo: https://github.com/AleksaC/hadolint-py | ||||||
|     rev: v2.12.0.3 |     rev: v2.12.0.3 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.python-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.python-version
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | 3.10.15 | ||||||
							
								
								
									
										87
									
								
								.ruff.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								.ruff.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | 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 | ||||||
|  |   "TC",   # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc | ||||||
|  |   "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,6 +5,5 @@ | |||||||
| /src-ui/ @paperless-ngx/frontend | /src-ui/ @paperless-ngx/frontend | ||||||
|  |  | ||||||
| /src/ @paperless-ngx/backend | /src/ @paperless-ngx/backend | ||||||
| pyproject.toml @paperless-ngx/backend | Pipfile* @paperless-ngx/backend | ||||||
| uv.lock @paperless-ngx/backend |  | ||||||
| *.py @paperless-ngx/backend | *.py @paperless-ngx/backend | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -26,11 +26,28 @@ esac | |||||||
| RUN set -eux \ | RUN set -eux \ | ||||||
|   && ./node_modules/.bin/ng build --configuration production |   && ./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 | # Stage: s6-overlay-base | ||||||
| # Purpose: Installs s6-overlay and rootfs | # Purpose: Installs s6-overlay and rootfs | ||||||
| # Comments: | # Comments: | ||||||
| #  - Don't leave anything extra in here either | #  - Don't leave anything extra in here either | ||||||
| FROM ghcr.io/astral-sh/uv:0.6.3-python3.12-bookworm-slim AS s6-overlay-base | FROM docker.io/python:3.12-slim-bookworm AS s6-overlay-base | ||||||
|  |  | ||||||
| WORKDIR /usr/src/s6 | WORKDIR /usr/src/s6 | ||||||
|  |  | ||||||
| @@ -106,12 +123,9 @@ ARG GS_VERSION=10.03.1 | |||||||
| # Set Python environment variables | # Set Python environment variables | ||||||
| ENV PYTHONDONTWRITEBYTECODE=1 \ | ENV PYTHONDONTWRITEBYTECODE=1 \ | ||||||
|     PYTHONUNBUFFERED=1 \ |     PYTHONUNBUFFERED=1 \ | ||||||
|     # Ignore warning from Whitenoise about async iterators |     # Ignore warning from Whitenoise | ||||||
|     PYTHONWARNINGS="ignore:::django.http.response:517" \ |     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 | # Begin installation and configuration | ||||||
| @@ -199,25 +213,36 @@ WORKDIR /usr/src/paperless/src/ | |||||||
|  |  | ||||||
| # Python dependencies | # Python dependencies | ||||||
| # Change pretty frequently | # Change pretty frequently | ||||||
| COPY --chown=1000:1000 ["pyproject.toml", "uv.lock", "/usr/src/paperless/src/"] | COPY --chown=1000:1000 --from=pipenv-base /usr/src/pipenv/requirements.txt ./ | ||||||
|  |  | ||||||
| # Packages needed only for building a few quick Python | # Packages needed only for building a few quick Python | ||||||
| # dependencies | # dependencies | ||||||
| ARG BUILD_PACKAGES="\ | ARG BUILD_PACKAGES="\ | ||||||
|   build-essential \ |   build-essential \ | ||||||
|  |   git \ | ||||||
|  |   # https://www.psycopg.org/docs/install.html#prerequisites | ||||||
|  |   libpq-dev \ | ||||||
|   # https://github.com/PyMySQL/mysqlclient#linux |   # https://github.com/PyMySQL/mysqlclient#linux | ||||||
|   default-libmysqlclient-dev \ |   default-libmysqlclient-dev \ | ||||||
|   pkg-config" |   pkg-config" | ||||||
|  |  | ||||||
|  | ARG ZXING_VERSION=2.3.0 | ||||||
|  | ARG PSYCOPG_VERSION=3.2.4 | ||||||
|  |  | ||||||
| # hadolint ignore=DL3042 | # hadolint ignore=DL3042 | ||||||
| RUN --mount=type=cache,target=${UV_CACHE_DIR},id=python-cache \ | RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \ | ||||||
|   set -eux \ |   set -eux \ | ||||||
|   && echo "Installing build system packages" \ |   && echo "Installing build system packages" \ | ||||||
|     && apt-get update \ |     && apt-get update \ | ||||||
|     && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ |     && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ | ||||||
|  |     && python3 -m pip install --upgrade wheel \ | ||||||
|   && echo "Installing Python requirements" \ |   && echo "Installing Python requirements" \ | ||||||
|     && uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \ |     && curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \ | ||||||
|     && uv pip install --system --no-python-downloads --python-preference system --requirements requirements.txt \ |       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 \ | ||||||
|   && echo "Installing NLTK data" \ |   && 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" snowball_data \ | ||||||
|     && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \ |     && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \ | ||||||
|   | |||||||
							
								
								
									
										99
									
								
								Pipfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								Pipfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | [[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 = "*" | ||||||
|  | granian = "*" | ||||||
|  | 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 = "*" | ||||||
|  | watchdog = "~=6.0" | ||||||
|  | whitenoise = "~=6.9" | ||||||
|  | whoosh = "~=2.7" | ||||||
|  | zxing-cpp = "*" | ||||||
|  |  | ||||||
|  | [dev-packages] | ||||||
|  | # Linting | ||||||
|  | pre-commit = "*" | ||||||
|  | ruff = "*" | ||||||
|  | # Testing | ||||||
|  | factory-boy = "*" | ||||||
|  | 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 = "*" | ||||||
							
								
								
									
										4812
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4812
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -509,12 +509,6 @@ 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`. | 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} | ## 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 | Paperless will attempt to "clean" certain invalid PDFs with `qpdf` before processing if, for example, the mime_type | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ first-time setup. | |||||||
|  |  | ||||||
|       Every command is executed directly from the root folder of the project unless specified otherwise. |       Every command is executed directly from the root folder of the project unless specified otherwise. | ||||||
|  |  | ||||||
| 1.  Install prerequisites + [uv](https://github.com/astral-sh/uv) as mentioned in | 1.  Install prerequisites + pipenv as mentioned in | ||||||
|     [Bare metal route](setup.md#bare_metal). |     [Bare metal route](setup.md#bare_metal). | ||||||
|  |  | ||||||
| 2.  Copy `paperless.conf.example` to `paperless.conf` and enable debug | 2.  Copy `paperless.conf.example` to `paperless.conf` and enable debug | ||||||
| @@ -75,13 +75,17 @@ first-time setup. | |||||||
| 4.  Install the Python dependencies: | 4.  Install the Python dependencies: | ||||||
|  |  | ||||||
|     ```bash |     ```bash | ||||||
|     $ uv sync --dev |     pipenv install --dev | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
|  |     !!! note | ||||||
|  |  | ||||||
|  |         Using a virtual environment is highly recommended. You can spawn one via `pipenv shell`. | ||||||
|  |  | ||||||
| 5.  Install pre-commit hooks: | 5.  Install pre-commit hooks: | ||||||
|  |  | ||||||
|     ```bash |     ```bash | ||||||
|     $ uv run pre-commit install |     pre-commit install | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
| 6.  Apply migrations and create a superuser for your development instance: | 6.  Apply migrations and create a superuser for your development instance: | ||||||
| @@ -89,8 +93,8 @@ first-time setup. | |||||||
|     ```bash |     ```bash | ||||||
|     # src/ |     # src/ | ||||||
|  |  | ||||||
|     $ uv run manage.py migrate |     python3 manage.py migrate | ||||||
|     $ uv run manage.py createsuperuser |     python3 manage.py createsuperuser | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
| 7.  You can now either ... | 7.  You can now either ... | ||||||
| @@ -160,19 +164,6 @@ $ ng build --configuration production | |||||||
|       complicated IF cases. Append `# noqa: E501` to disable this check |       complicated IF cases. Append `# noqa: E501` to disable this check | ||||||
|       for certain lines. |       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 | ## Front end development | ||||||
|  |  | ||||||
| The front end is built using AngularJS. In order to get started, you need Node.js (version 14.15+) and | The front end is built using AngularJS. In order to get started, you need Node.js (version 14.15+) and | ||||||
| @@ -341,21 +332,27 @@ LANGUAGES = [ | |||||||
| The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/). | 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: | If you want to build the documentation locally, this is how you do it: | ||||||
|  |  | ||||||
| 1.  Build the documentation | 1.  Have an active pipenv shell (`pipenv shell`) and install Python dependencies: | ||||||
|  |  | ||||||
|     ```bash |     ```bash | ||||||
|     $ uv run mkdocs build --config-file mkdocs.yml |     pipenv install --dev | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  | 2.  Build the documentation | ||||||
|  |  | ||||||
|  |     ```bash | ||||||
|  |     mkdocs build --config-file mkdocs.yml | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
|     _alternatively..._ |     _alternatively..._ | ||||||
|  |  | ||||||
| 2.  Serve the documentation. This will spin up a | 3.  Serve the documentation. This will spin up a | ||||||
|     copy of the documentation at http://127.0.0.1:8000 |     copy of the documentation at http://127.0.0.1:8000 | ||||||
|     that will automatically refresh every time you change |     that will automatically refresh every time you change | ||||||
|     something. |     something. | ||||||
|  |  | ||||||
|     ```bash |     ```bash | ||||||
|     $ uv run mkdocs serve |     mkdocs serve | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
| ## Building the Docker image | ## Building the Docker image | ||||||
|   | |||||||
| @@ -380,12 +380,6 @@ are released, dependency support is confirmed, etc. | |||||||
|         dependencies.  This is an alternative to the above and may require adjusting |         dependencies.  This is an alternative to the above and may require adjusting | ||||||
|         the example scripts to utilize the virtual environment paths |         the example scripts to utilize the virtual environment paths | ||||||
|  |  | ||||||
|     !!! 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 commands: | 9.  Go to `/opt/paperless/src`, and execute the following commands: | ||||||
|  |  | ||||||
|     ```bash |     ```bash | ||||||
|   | |||||||
							
								
								
									
										401
									
								
								pyproject.toml
									
									
									
									
									
								
							
							
						
						
									
										401
									
								
								pyproject.toml
									
									
									
									
									
								
							| @@ -1,401 +0,0 @@ | |||||||
| [project] |  | ||||||
| name = "paperless-ngx" |  | ||||||
| version = "2.14.7" |  | ||||||
| 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.4.0", |  | ||||||
|   "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.6", |  | ||||||
|   "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.2.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.9.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~=2.7", |  | ||||||
|   "zxing-cpp~=2.3.0", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| optional-dependencies.mariadb = [ |  | ||||||
|   "mysqlclient~=2.2.7", |  | ||||||
| ] |  | ||||||
| optional-dependencies.postgres = [ |  | ||||||
|   "psycopg[c]==3.2.4", |  | ||||||
|   # Direct dependency for proper resolution of the pre-built wheels |  | ||||||
|   "psycopg-c==3.2.4", |  | ||||||
| ] |  | ||||||
| optional-dependencies.webserver = [ |  | ||||||
|   "granian~=1.7.6", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [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/0012_auto_20160305_0040.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/documents/migrations/0014_document_checksum.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/documents/migrations/1003_mime_types.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" = [ |  | ||||||
|   "PTH", |  | ||||||
|   "SIM115", |  | ||||||
| ] # TODO PTH Enable & remove |  | ||||||
| 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/tasks.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/documents/tests/test_api_app_config.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/documents/tests/test_classifier.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_management_thumbnails.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/tests/test_tasks.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/documents/tests/test_views.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/tests/test_checks.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/paperless/urls.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_mail/preprocessor.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/paperless_tesseract/parsers.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [ |  | ||||||
|   "PTH", |  | ||||||
|   "RUF001", |  | ||||||
| ] # TODO PTH Enable & remove |  | ||||||
| lint.per-file-ignores."src/paperless_tika/tests/test_live_tika.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO Enable & remove |  | ||||||
| lint.per-file-ignores."src/paperless_tika/tests/test_tika_parser.py" = [ |  | ||||||
|   "PTH", |  | ||||||
| ] # TODO 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.4/psycopg_c-3.2.4-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.4/psycopg_c-3.2.4-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" |  | ||||||
| @@ -12,13 +12,4 @@ module.exports = { | |||||||
|     '^src/(.*)': '<rootDir>/src/$1', |     '^src/(.*)': '<rootDir>/src/$1', | ||||||
|   }, |   }, | ||||||
|   workerIdleMemoryLimit: '512MB', |   workerIdleMemoryLimit: '512MB', | ||||||
|   reporters: [ |  | ||||||
|     'default', |  | ||||||
|     [ |  | ||||||
|       'jest-junit', |  | ||||||
|       { |  | ||||||
|         classNameTemplate: '{filepath}/{classname}: {title}', |  | ||||||
|       }, |  | ||||||
|     ], |  | ||||||
|   ], |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -545,7 +545,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">360</context> |           <context context-type="linenumber">364</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context> | ||||||
| @@ -1034,8 +1034,8 @@ | |||||||
|           <context context-type="linenumber">188</context> |           <context context-type="linenumber">188</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2678648946508279627" datatype="html"> |       <trans-unit id="7791089127901960679" datatype="html"> | ||||||
|         <source>Default zoom</source> |         <source>Default zoom:</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">194</context> |           <context context-type="linenumber">194</context> | ||||||
| @@ -1076,79 +1076,11 @@ | |||||||
|           <context context-type="linenumber">213</context> |           <context context-type="linenumber">213</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6760166989231109310" datatype="html"> |  | ||||||
|         <source>Global search</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">217</context> |  | ||||||
|         </context-group> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> |  | ||||||
|           <context context-type="linenumber">120</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="2818183879511244335" datatype="html"> |  | ||||||
|         <source>Do not include advanced search results</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">220</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="3969258421469113318" datatype="html"> |  | ||||||
|         <source>Full search links to</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">226</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="6631288852577115923" datatype="html"> |  | ||||||
|         <source>Title and content search</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">230</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="1010505078885609376" datatype="html"> |  | ||||||
|         <source>Advanced search</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">231</context> |  | ||||||
|         </context-group> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> |  | ||||||
|           <context context-type="linenumber">24</context> |  | ||||||
|         </context-group> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |  | ||||||
|           <context context-type="linenumber">173</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="8508424367627989968" datatype="html"> |  | ||||||
|         <source>Bulk editing</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">236</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="8158899674926420054" datatype="html"> |  | ||||||
|         <source>Show confirmation dialogs</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">239</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="290238406234356122" datatype="html"> |  | ||||||
|         <source>Apply on close</source> |  | ||||||
|         <context-group purpose="location"> |  | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |  | ||||||
|           <context context-type="linenumber">240</context> |  | ||||||
|         </context-group> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="8104421162933956065" datatype="html"> |       <trans-unit id="8104421162933956065" datatype="html"> | ||||||
|         <source>Notes</source> |         <source>Notes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">244</context> |           <context context-type="linenumber">217</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> | ||||||
| @@ -1165,16 +1097,84 @@ | |||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="293524471897878391" datatype="html"> |       <trans-unit id="293524471897878391" datatype="html"> | ||||||
|         <source>Enable notes</source> |         <source>Enable notes</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">220</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="8508424367627989968" datatype="html"> | ||||||
|  |         <source>Bulk editing</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">224</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="8158899674926420054" datatype="html"> | ||||||
|  |         <source>Show confirmation dialogs</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">227</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="290238406234356122" datatype="html"> | ||||||
|  |         <source>Apply on close</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">228</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="6760166989231109310" datatype="html"> | ||||||
|  |         <source>Global search</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">232</context> | ||||||
|  |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> | ||||||
|  |           <context context-type="linenumber">120</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="2818183879511244335" datatype="html"> | ||||||
|  |         <source>Do not include advanced search results</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">235</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="3969258421469113318" datatype="html"> | ||||||
|  |         <source>Full search links to</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">243</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="6631288852577115923" datatype="html"> | ||||||
|  |         <source>Title and content search</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">247</context> |           <context context-type="linenumber">247</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="1010505078885609376" datatype="html"> | ||||||
|  |         <source>Advanced search</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|  |           <context context-type="linenumber">248</context> | ||||||
|  |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> | ||||||
|  |           <context context-type="linenumber">24</context> | ||||||
|  |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||||
|  |           <context context-type="linenumber">173</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="7314814725704332646" datatype="html"> |       <trans-unit id="7314814725704332646" datatype="html"> | ||||||
|         <source>Permissions</source> |         <source>Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">258</context> |           <context context-type="linenumber">262</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html</context> | ||||||
| @@ -1237,28 +1237,28 @@ | |||||||
|         <source>Default Permissions</source> |         <source>Default Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">261</context> |           <context context-type="linenumber">265</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6544153565064275581" datatype="html"> |       <trans-unit id="8222269449891326545" datatype="html"> | ||||||
|         <source> Settings apply to this user account for objects (Tags, Mail Rules, etc. but not documents) created via the web UI. </source> |         <source> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI </source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">265,267</context> |           <context context-type="linenumber">269,271</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4292903881380648974" datatype="html"> |       <trans-unit id="4292903881380648974" datatype="html"> | ||||||
|         <source>Default Owner</source> |         <source>Default Owner</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">272</context> |           <context context-type="linenumber">276</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="734147282056744882" datatype="html"> |       <trans-unit id="734147282056744882" datatype="html"> | ||||||
|         <source>Objects without an owner can be viewed and edited by all users</source> |         <source>Objects without an owner can be viewed and edited by all users</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">276</context> |           <context context-type="linenumber">280</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> |           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> | ||||||
| @@ -1269,18 +1269,18 @@ | |||||||
|         <source>Default View Permissions</source> |         <source>Default View Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">281</context> |           <context context-type="linenumber">285</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2191775412581217688" datatype="html"> |       <trans-unit id="2191775412581217688" datatype="html"> | ||||||
|         <source>Users:</source> |         <source>Users:</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">286</context> |           <context context-type="linenumber">290</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">313</context> |           <context context-type="linenumber">317</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> | ||||||
| @@ -1311,11 +1311,11 @@ | |||||||
|         <source>Groups:</source> |         <source>Groups:</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">296</context> |           <context context-type="linenumber">300</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">323</context> |           <context context-type="linenumber">327</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> | ||||||
| @@ -1346,14 +1346,14 @@ | |||||||
|         <source>Default Edit Permissions</source> |         <source>Default Edit Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">308</context> |           <context context-type="linenumber">312</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3728984448750213892" datatype="html"> |       <trans-unit id="3728984448750213892" datatype="html"> | ||||||
|         <source>Edit permissions also grant viewing permissions</source> |         <source>Edit permissions also grant viewing permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">332</context> |           <context context-type="linenumber">336</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> | ||||||
| @@ -1372,7 +1372,7 @@ | |||||||
|         <source>Notifications</source> |         <source>Notifications</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">340</context> |           <context context-type="linenumber">344</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html</context> |           <context context-type="sourcefile">src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html</context> | ||||||
| @@ -1383,49 +1383,49 @@ | |||||||
|         <source>Document processing</source> |         <source>Document processing</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">343</context> |           <context context-type="linenumber">347</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3656786776644872398" datatype="html"> |       <trans-unit id="3656786776644872398" datatype="html"> | ||||||
|         <source>Show notifications when new documents are detected</source> |         <source>Show notifications when new documents are detected</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">347</context> |           <context context-type="linenumber">351</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6057053428592387613" datatype="html"> |       <trans-unit id="6057053428592387613" datatype="html"> | ||||||
|         <source>Show notifications when document processing completes successfully</source> |         <source>Show notifications when document processing completes successfully</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">348</context> |           <context context-type="linenumber">352</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="370315664367425513" datatype="html"> |       <trans-unit id="370315664367425513" datatype="html"> | ||||||
|         <source>Show notifications when document processing fails</source> |         <source>Show notifications when document processing fails</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">349</context> |           <context context-type="linenumber">353</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6838309441164918531" datatype="html"> |       <trans-unit id="6838309441164918531" datatype="html"> | ||||||
|         <source>Suppress notifications on dashboard</source> |         <source>Suppress notifications on dashboard</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">350</context> |           <context context-type="linenumber">354</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2741919327232918179" datatype="html"> |       <trans-unit id="2741919327232918179" datatype="html"> | ||||||
|         <source>This will suppress all messages about document processing status on the dashboard.</source> |         <source>This will suppress all messages about document processing status on the dashboard.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">350</context> |           <context context-type="linenumber">354</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2159130950882492111" datatype="html"> |       <trans-unit id="2159130950882492111" datatype="html"> | ||||||
|         <source>Cancel</source> |         <source>Cancel</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> | ||||||
|           <context context-type="linenumber">361</context> |           <context context-type="linenumber">365</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context> |           <context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context> | ||||||
|   | |||||||
							
								
								
									
										245
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										245
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -44,24 +44,23 @@ | |||||||
|         "@angular-devkit/build-angular": "^19.0.4", |         "@angular-devkit/build-angular": "^19.0.4", | ||||||
|         "@angular-devkit/core": "^19.2.0", |         "@angular-devkit/core": "^19.2.0", | ||||||
|         "@angular-devkit/schematics": "^19.2.0", |         "@angular-devkit/schematics": "^19.2.0", | ||||||
|         "@angular-eslint/builder": "19.2.0", |         "@angular-eslint/builder": "19.1.0", | ||||||
|         "@angular-eslint/eslint-plugin": "19.2.0", |         "@angular-eslint/eslint-plugin": "19.1.0", | ||||||
|         "@angular-eslint/eslint-plugin-template": "19.2.0", |         "@angular-eslint/eslint-plugin-template": "19.1.0", | ||||||
|         "@angular-eslint/schematics": "19.2.0", |         "@angular-eslint/schematics": "19.1.0", | ||||||
|         "@angular-eslint/template-parser": "19.2.0", |         "@angular-eslint/template-parser": "19.1.0", | ||||||
|         "@angular/cli": "~19.2.0", |         "@angular/cli": "~19.2.0", | ||||||
|         "@angular/compiler-cli": "~19.2.0", |         "@angular/compiler-cli": "~19.2.0", | ||||||
|         "@codecov/webpack-plugin": "^1.9.0", |         "@codecov/webpack-plugin": "^1.9.0", | ||||||
|         "@playwright/test": "^1.50.1", |         "@playwright/test": "^1.50.1", | ||||||
|         "@types/jest": "^29.5.14", |         "@types/jest": "^29.5.14", | ||||||
|         "@types/node": "^22.13.9", |         "@types/node": "^22.13.5", | ||||||
|         "@typescript-eslint/eslint-plugin": "^8.26.0", |         "@typescript-eslint/eslint-plugin": "^8.25.0", | ||||||
|         "@typescript-eslint/parser": "^8.26.0", |         "@typescript-eslint/parser": "^8.25.0", | ||||||
|         "@typescript-eslint/utils": "^8.0.0", |         "@typescript-eslint/utils": "^8.0.0", | ||||||
|         "eslint": "^9.21.0", |         "eslint": "^9.21.0", | ||||||
|         "jest": "29.7.0", |         "jest": "29.7.0", | ||||||
|         "jest-environment-jsdom": "^29.7.0", |         "jest-environment-jsdom": "^29.7.0", | ||||||
|         "jest-junit": "^16.0.0", |  | ||||||
|         "jest-preset-angular": "^14.5.3", |         "jest-preset-angular": "^14.5.3", | ||||||
|         "jest-websocket-mock": "^2.5.0", |         "jest-websocket-mock": "^2.5.0", | ||||||
|         "patch-package": "^8.0.0", |         "patch-package": "^8.0.0", | ||||||
| @@ -1149,9 +1148,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@angular-eslint/builder": { |     "node_modules/@angular-eslint/builder": { | ||||||
|       "version": "19.2.0", |       "version": "19.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.2.0.tgz", |       "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.1.0.tgz", | ||||||
|       "integrity": "sha512-8Lx24MrMJT8RlgDtwqfiLiJo4DzSaktjco6RmELUdWO2chJgRe9y+2iIgOeB2pmyD9UCsubwsfjBXlrnV/MPhQ==", |       "integrity": "sha512-LWdQMTES/7GySlpTNFJn3k33ZGmjjWlHI/+IHV7B3xHQ9hj4MPK4ACmE/PNOAIQ9LwQm7sKS+3cTMxOZQ/cvSg==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
| @@ -1164,21 +1163,21 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@angular-eslint/bundled-angular-compiler": { |     "node_modules/@angular-eslint/bundled-angular-compiler": { | ||||||
|       "version": "19.2.0", |       "version": "19.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.2.0.tgz", |       "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.1.0.tgz", | ||||||
|       "integrity": "sha512-hmmAogTpYGbBvnJ0j7DNLi8YQ+YEEuwFdx0heU8XjTpZlRoSRIP7MJJVlaQCt+ZT5f5XwdGtqi9lOXqqcyGHLA==", |       "integrity": "sha512-HUJyukRvnh8Z9lIdxdblBRuBaPYEVv4iAYZMw3d+dn4rrM27Nt5oh3/zkwYrrPkt36tZdeXdDWrOuz9jgjVN5w==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@angular-eslint/eslint-plugin": { |     "node_modules/@angular-eslint/eslint-plugin": { | ||||||
|       "version": "19.2.0", |       "version": "19.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.2.0.tgz", |       "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.1.0.tgz", | ||||||
|       "integrity": "sha512-QQWWDrTdJ22tBd7RLFG/FdPwNyYEhg7YwWgn29z6XcdnV00ZFtf7FRbv/te1kqVNPvfjtht7bvtHcPQ432aUdQ==", |       "integrity": "sha512-TDO0+Ry+oNkxnaLHogKp1k2aey6IkJef5d7hathE4UFT6owjRizltWaRoX6bGw7Qu1yagVLL8L2Se8SddxSPAQ==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@angular-eslint/bundled-angular-compiler": "19.2.0", |         "@angular-eslint/bundled-angular-compiler": "19.1.0", | ||||||
|         "@angular-eslint/utils": "19.2.0" |         "@angular-eslint/utils": "19.1.0" | ||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", |         "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", | ||||||
| @@ -1187,14 +1186,14 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@angular-eslint/eslint-plugin-template": { |     "node_modules/@angular-eslint/eslint-plugin-template": { | ||||||
|       "version": "19.2.0", |       "version": "19.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.2.0.tgz", |       "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.1.0.tgz", | ||||||
|       "integrity": "sha512-lUSzmk5/Dr0bNc2Omb5CZDu3zQZh70bJyuXnN5MKd00V1b3u90eqvMSveFzWFJ6Eot8Hh8+FxtiozPwGqOE+Og==", |       "integrity": "sha512-bIUizkCY40mnU8oAO1tLV7uN2H/cHf1evLlhpqlb9JYwc5dT2moiEhNDo61OtOgkJmDGNuThAeO9Xk9hGQc7nA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@angular-eslint/bundled-angular-compiler": "19.2.0", |         "@angular-eslint/bundled-angular-compiler": "19.1.0", | ||||||
|         "@angular-eslint/utils": "19.2.0", |         "@angular-eslint/utils": "19.1.0", | ||||||
|         "aria-query": "5.3.2", |         "aria-query": "5.3.2", | ||||||
|         "axobject-query": "4.1.0" |         "axobject-query": "4.1.0" | ||||||
|       }, |       }, | ||||||
| @@ -1205,47 +1204,17 @@ | |||||||
|         "typescript": "*" |         "typescript": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@angular-eslint/utils": { |  | ||||||
|       "version": "19.2.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.2.0.tgz", |  | ||||||
|       "integrity": "sha512-1XQXzIqYadKUxcAgW1DPev56SVbR8Uld6TthgolU7rfIX23RYMIIRtQlrQCk7zoXLXm5fzcGqjTR4wHfoD+iWg==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |  | ||||||
|         "@angular-eslint/bundled-angular-compiler": "19.2.0" |  | ||||||
|       }, |  | ||||||
|       "peerDependencies": { |  | ||||||
|         "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", |  | ||||||
|         "eslint": "^8.57.0 || ^9.0.0", |  | ||||||
|         "typescript": "*" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/@angular-eslint/eslint-plugin/node_modules/@angular-eslint/utils": { |  | ||||||
|       "version": "19.2.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.2.0.tgz", |  | ||||||
|       "integrity": "sha512-1XQXzIqYadKUxcAgW1DPev56SVbR8Uld6TthgolU7rfIX23RYMIIRtQlrQCk7zoXLXm5fzcGqjTR4wHfoD+iWg==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |  | ||||||
|         "@angular-eslint/bundled-angular-compiler": "19.2.0" |  | ||||||
|       }, |  | ||||||
|       "peerDependencies": { |  | ||||||
|         "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", |  | ||||||
|         "eslint": "^8.57.0 || ^9.0.0", |  | ||||||
|         "typescript": "*" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/@angular-eslint/schematics": { |     "node_modules/@angular-eslint/schematics": { | ||||||
|       "version": "19.2.0", |       "version": "19.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.2.0.tgz", |       "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.1.0.tgz", | ||||||
|       "integrity": "sha512-SQfbKgPEJNkK5TVXRsdnWp6TjvVZOczvf8lELF1n+I/Uwmp7ulUjTRgTo59ZQnXoPSs2qCPgS4gAOVR6CD91zQ==", |       "integrity": "sha512-6S1FjmM7rZxc0u0W0KjqWYOkFQ0q89IGyjPkdUt1a8NwRnWg3VoXp4WYfeuZOjda/FEYuBS/E6rckLAMp0h6Aw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@angular-devkit/core": ">= 19.0.0 < 20.0.0", |         "@angular-devkit/core": ">= 19.0.0 < 20.0.0", | ||||||
|         "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", |         "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", | ||||||
|         "@angular-eslint/eslint-plugin": "19.2.0", |         "@angular-eslint/eslint-plugin": "19.1.0", | ||||||
|         "@angular-eslint/eslint-plugin-template": "19.2.0", |         "@angular-eslint/eslint-plugin-template": "19.1.0", | ||||||
|         "ignore": "7.0.3", |         "ignore": "7.0.3", | ||||||
|         "semver": "7.7.1", |         "semver": "7.7.1", | ||||||
|         "strip-json-comments": "3.1.1" |         "strip-json-comments": "3.1.1" | ||||||
| @@ -1262,13 +1231,13 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@angular-eslint/template-parser": { |     "node_modules/@angular-eslint/template-parser": { | ||||||
|       "version": "19.2.0", |       "version": "19.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.2.0.tgz", |       "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.1.0.tgz", | ||||||
|       "integrity": "sha512-VqgvFrILhoMe0GHZrx+Bjy8kx7/LJfJTd+x/wzE/X1cCChSU81MBZFMVeFMnoI75OOQUf4fwaaKrtUhUvAkVyw==", |       "integrity": "sha512-wbMi7adlC+uYqZo7NHNBShpNhFJRZsXLqihqvFpAUt1Ei6uDX8HR6MyMEDZ9tUnlqtPVW5nmbedPyLVG7HkjAA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@angular-eslint/bundled-angular-compiler": "19.2.0", |         "@angular-eslint/bundled-angular-compiler": "19.1.0", | ||||||
|         "eslint-scope": "^8.0.2" |         "eslint-scope": "^8.0.2" | ||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
| @@ -1276,6 +1245,21 @@ | |||||||
|         "typescript": "*" |         "typescript": "*" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@angular-eslint/utils": { | ||||||
|  |       "version": "19.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.1.0.tgz", | ||||||
|  |       "integrity": "sha512-mcb7hPMH/u6wwUwvsewrmgb9y9NWN6ZacvpUvKlTOxF/jOtTdsu0XfV4YB43sp2A8NWzYzX0Str4c8K1xSmuBQ==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@angular-eslint/bundled-angular-compiler": "19.1.0" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", | ||||||
|  |         "eslint": "^8.57.0 || ^9.0.0", | ||||||
|  |         "typescript": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@angular/cdk": { |     "node_modules/@angular/cdk": { | ||||||
|       "version": "19.2.1", |       "version": "19.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.1.tgz", |       "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.1.tgz", | ||||||
| @@ -7166,9 +7150,9 @@ | |||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@types/node": { |     "node_modules/@types/node": { | ||||||
|       "version": "22.13.9", |       "version": "22.13.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", |       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", | ||||||
|       "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", |       "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
| @@ -7287,17 +7271,17 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/eslint-plugin": { |     "node_modules/@typescript-eslint/eslint-plugin": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", | ||||||
|       "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", |       "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@eslint-community/regexpp": "^4.10.0", |         "@eslint-community/regexpp": "^4.10.0", | ||||||
|         "@typescript-eslint/scope-manager": "8.26.0", |         "@typescript-eslint/scope-manager": "8.25.0", | ||||||
|         "@typescript-eslint/type-utils": "8.26.0", |         "@typescript-eslint/type-utils": "8.25.0", | ||||||
|         "@typescript-eslint/utils": "8.26.0", |         "@typescript-eslint/utils": "8.25.0", | ||||||
|         "@typescript-eslint/visitor-keys": "8.26.0", |         "@typescript-eslint/visitor-keys": "8.25.0", | ||||||
|         "graphemer": "^1.4.0", |         "graphemer": "^1.4.0", | ||||||
|         "ignore": "^5.3.1", |         "ignore": "^5.3.1", | ||||||
|         "natural-compare": "^1.4.0", |         "natural-compare": "^1.4.0", | ||||||
| @@ -7313,20 +7297,20 @@ | |||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", |         "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", | ||||||
|         "eslint": "^8.57.0 || ^9.0.0", |         "eslint": "^8.57.0 || ^9.0.0", | ||||||
|         "typescript": ">=4.8.4 <5.9.0" |         "typescript": ">=4.8.4 <5.8.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/parser": { |     "node_modules/@typescript-eslint/parser": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", | ||||||
|       "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", |       "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/scope-manager": "8.26.0", |         "@typescript-eslint/scope-manager": "8.25.0", | ||||||
|         "@typescript-eslint/types": "8.26.0", |         "@typescript-eslint/types": "8.25.0", | ||||||
|         "@typescript-eslint/typescript-estree": "8.26.0", |         "@typescript-eslint/typescript-estree": "8.25.0", | ||||||
|         "@typescript-eslint/visitor-keys": "8.26.0", |         "@typescript-eslint/visitor-keys": "8.25.0", | ||||||
|         "debug": "^4.3.4" |         "debug": "^4.3.4" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
| @@ -7338,18 +7322,18 @@ | |||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "eslint": "^8.57.0 || ^9.0.0", |         "eslint": "^8.57.0 || ^9.0.0", | ||||||
|         "typescript": ">=4.8.4 <5.9.0" |         "typescript": ">=4.8.4 <5.8.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/scope-manager": { |     "node_modules/@typescript-eslint/scope-manager": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", | ||||||
|       "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", |       "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/types": "8.26.0", |         "@typescript-eslint/types": "8.25.0", | ||||||
|         "@typescript-eslint/visitor-keys": "8.26.0" |         "@typescript-eslint/visitor-keys": "8.25.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |         "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
| @@ -7360,14 +7344,14 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/type-utils": { |     "node_modules/@typescript-eslint/type-utils": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", | ||||||
|       "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", |       "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/typescript-estree": "8.26.0", |         "@typescript-eslint/typescript-estree": "8.25.0", | ||||||
|         "@typescript-eslint/utils": "8.26.0", |         "@typescript-eslint/utils": "8.25.0", | ||||||
|         "debug": "^4.3.4", |         "debug": "^4.3.4", | ||||||
|         "ts-api-utils": "^2.0.1" |         "ts-api-utils": "^2.0.1" | ||||||
|       }, |       }, | ||||||
| @@ -7380,13 +7364,13 @@ | |||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "eslint": "^8.57.0 || ^9.0.0", |         "eslint": "^8.57.0 || ^9.0.0", | ||||||
|         "typescript": ">=4.8.4 <5.9.0" |         "typescript": ">=4.8.4 <5.8.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/types": { |     "node_modules/@typescript-eslint/types": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", | ||||||
|       "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", |       "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "engines": { |       "engines": { | ||||||
| @@ -7398,14 +7382,14 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/typescript-estree": { |     "node_modules/@typescript-eslint/typescript-estree": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", | ||||||
|       "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", |       "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/types": "8.26.0", |         "@typescript-eslint/types": "8.25.0", | ||||||
|         "@typescript-eslint/visitor-keys": "8.26.0", |         "@typescript-eslint/visitor-keys": "8.25.0", | ||||||
|         "debug": "^4.3.4", |         "debug": "^4.3.4", | ||||||
|         "fast-glob": "^3.3.2", |         "fast-glob": "^3.3.2", | ||||||
|         "is-glob": "^4.0.3", |         "is-glob": "^4.0.3", | ||||||
| @@ -7421,7 +7405,7 @@ | |||||||
|         "url": "https://opencollective.com/typescript-eslint" |         "url": "https://opencollective.com/typescript-eslint" | ||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "typescript": ">=4.8.4 <5.9.0" |         "typescript": ">=4.8.4 <5.8.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { |     "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { | ||||||
| @@ -7451,16 +7435,16 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/utils": { |     "node_modules/@typescript-eslint/utils": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", | ||||||
|       "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", |       "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@eslint-community/eslint-utils": "^4.4.0", |         "@eslint-community/eslint-utils": "^4.4.0", | ||||||
|         "@typescript-eslint/scope-manager": "8.26.0", |         "@typescript-eslint/scope-manager": "8.25.0", | ||||||
|         "@typescript-eslint/types": "8.26.0", |         "@typescript-eslint/types": "8.25.0", | ||||||
|         "@typescript-eslint/typescript-estree": "8.26.0" |         "@typescript-eslint/typescript-estree": "8.25.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |         "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
| @@ -7471,17 +7455,17 @@ | |||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "eslint": "^8.57.0 || ^9.0.0", |         "eslint": "^8.57.0 || ^9.0.0", | ||||||
|         "typescript": ">=4.8.4 <5.9.0" |         "typescript": ">=4.8.4 <5.8.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@typescript-eslint/visitor-keys": { |     "node_modules/@typescript-eslint/visitor-keys": { | ||||||
|       "version": "8.26.0", |       "version": "8.25.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", |       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", | ||||||
|       "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", |       "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@typescript-eslint/types": "8.26.0", |         "@typescript-eslint/types": "8.25.0", | ||||||
|         "eslint-visitor-keys": "^4.2.0" |         "eslint-visitor-keys": "^4.2.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
| @@ -12419,32 +12403,6 @@ | |||||||
|         "fsevents": "^2.3.2" |         "fsevents": "^2.3.2" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/jest-junit": { |  | ||||||
|       "version": "16.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", |  | ||||||
|       "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "Apache-2.0", |  | ||||||
|       "dependencies": { |  | ||||||
|         "mkdirp": "^1.0.4", |  | ||||||
|         "strip-ansi": "^6.0.1", |  | ||||||
|         "uuid": "^8.3.2", |  | ||||||
|         "xml": "^1.0.1" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=10.12.0" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/jest-junit/node_modules/uuid": { |  | ||||||
|       "version": "8.3.2", |  | ||||||
|       "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", |  | ||||||
|       "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT", |  | ||||||
|       "bin": { |  | ||||||
|         "uuid": "dist/bin/uuid" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/jest-leak-detector": { |     "node_modules/jest-leak-detector": { | ||||||
|       "version": "29.7.0", |       "version": "29.7.0", | ||||||
|       "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", |       "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", | ||||||
| @@ -19292,13 +19250,6 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/xml": { |  | ||||||
|       "version": "1.0.1", |  | ||||||
|       "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", |  | ||||||
|       "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", |  | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT" |  | ||||||
|     }, |  | ||||||
|     "node_modules/xml-name-validator": { |     "node_modules/xml-name-validator": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", | ||||||
|   | |||||||
| @@ -46,24 +46,23 @@ | |||||||
|     "@angular-devkit/build-angular": "^19.0.4", |     "@angular-devkit/build-angular": "^19.0.4", | ||||||
|     "@angular-devkit/core": "^19.2.0", |     "@angular-devkit/core": "^19.2.0", | ||||||
|     "@angular-devkit/schematics": "^19.2.0", |     "@angular-devkit/schematics": "^19.2.0", | ||||||
|     "@angular-eslint/builder": "19.2.0", |     "@angular-eslint/builder": "19.1.0", | ||||||
|     "@angular-eslint/eslint-plugin": "19.2.0", |     "@angular-eslint/eslint-plugin": "19.1.0", | ||||||
|     "@angular-eslint/eslint-plugin-template": "19.2.0", |     "@angular-eslint/eslint-plugin-template": "19.1.0", | ||||||
|     "@angular-eslint/schematics": "19.2.0", |     "@angular-eslint/schematics": "19.1.0", | ||||||
|     "@angular-eslint/template-parser": "19.2.0", |     "@angular-eslint/template-parser": "19.1.0", | ||||||
|     "@angular/cli": "~19.2.0", |     "@angular/cli": "~19.2.0", | ||||||
|     "@angular/compiler-cli": "~19.2.0", |     "@angular/compiler-cli": "~19.2.0", | ||||||
|     "@codecov/webpack-plugin": "^1.9.0", |     "@codecov/webpack-plugin": "^1.9.0", | ||||||
|     "@playwright/test": "^1.50.1", |     "@playwright/test": "^1.50.1", | ||||||
|     "@types/jest": "^29.5.14", |     "@types/jest": "^29.5.14", | ||||||
|     "@types/node": "^22.13.9", |     "@types/node": "^22.13.5", | ||||||
|     "@typescript-eslint/eslint-plugin": "^8.26.0", |     "@typescript-eslint/eslint-plugin": "^8.25.0", | ||||||
|     "@typescript-eslint/parser": "^8.26.0", |     "@typescript-eslint/parser": "^8.25.0", | ||||||
|     "@typescript-eslint/utils": "^8.0.0", |     "@typescript-eslint/utils": "^8.0.0", | ||||||
|     "eslint": "^9.21.0", |     "eslint": "^9.21.0", | ||||||
|     "jest": "29.7.0", |     "jest": "29.7.0", | ||||||
|     "jest-environment-jsdom": "^29.7.0", |     "jest-environment-jsdom": "^29.7.0", | ||||||
|     "jest-junit": "^16.0.0", |  | ||||||
|     "jest-preset-angular": "^14.5.3", |     "jest-preset-angular": "^14.5.3", | ||||||
|     "jest-websocket-mock": "^2.5.0", |     "jest-websocket-mock": "^2.5.0", | ||||||
|     "patch-package": "^8.0.0", |     "patch-package": "^8.0.0", | ||||||
|   | |||||||
| @@ -118,7 +118,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <div class="row"> |             <div class="row mb-3"> | ||||||
|               <div class="col-md-3 col-form-label pt-0"> |               <div class="col-md-3 col-form-label pt-0"> | ||||||
|                 <span i18n>Sidebar</span> |                 <span i18n>Sidebar</span> | ||||||
|               </div> |               </div> | ||||||
| @@ -129,7 +129,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <div class="row"> |             <div class="row mb-3"> | ||||||
|               <div class="col-md-3 col-form-label pt-0"> |               <div class="col-md-3 col-form-label pt-0"> | ||||||
|                 <span i18n>Dark mode</span> |                 <span i18n>Dark mode</span> | ||||||
|               </div> |               </div> | ||||||
| @@ -165,7 +165,7 @@ | |||||||
|                   <p i18n> |                   <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. |                     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"> |                   <p> | ||||||
|                     <em i18n>No tracking data is collected by the app in any way.</em> |                     <em i18n>No tracking data is collected by the app in any way.</em> | ||||||
|                   </p> |                   </p> | ||||||
|                 </ng-template> |                 </ng-template> | ||||||
| @@ -173,7 +173,7 @@ | |||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <h5 class="mt-3" i18n>Saved Views</h5> |             <h5 class="mt-3" i18n>Saved Views</h5> | ||||||
|             <div class="row"> |             <div class="row mb-3"> | ||||||
|               <div class="col"> |               <div class="col"> | ||||||
|                 <pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check> |                 <pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check> | ||||||
|               </div> |               </div> | ||||||
| @@ -183,15 +183,15 @@ | |||||||
|           <div class="col-xl-6 ps-xl-5"> |           <div class="col-xl-6 ps-xl-5"> | ||||||
|             <h5 class="mt-3 mt-md-0" i18n>Document editing</h5> |             <h5 class="mt-3 mt-md-0" i18n>Document editing</h5> | ||||||
|  |  | ||||||
|             <div class="row"> |             <div class="row mb-3"> | ||||||
|               <div class="col"> |               <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> |                 <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> |             </div> | ||||||
|  |  | ||||||
|             <div class="row"> |             <div class="row mb-3"> | ||||||
|               <div class="col-md-3 col-form-label pt-0"> |               <div class="col-2"> | ||||||
|                 <span i18n>Default zoom</span> |                 <span i18n>Default zoom:</span> | ||||||
|               </div> |               </div> | ||||||
|               <div class="col"> |               <div class="col"> | ||||||
|                 <select class="form-select" formControlName="pdfViewerDefaultZoom"> |                 <select class="form-select" formControlName="pdfViewerDefaultZoom"> | ||||||
| @@ -202,7 +202,7 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <div class="row"> |             <div class="row mb-3"> | ||||||
|               <div class="col"> |               <div class="col"> | ||||||
|                 <pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check> |                 <pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check> | ||||||
|               </div> |               </div> | ||||||
| @@ -214,22 +214,10 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <h5 class="mt-3" i18n>Global search</h5> |             <h5 class="mt-3" i18n>Notes</h5> | ||||||
|             <div class="row"> |  | ||||||
|               <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="row mb-3"> | ||||||
|               <div class="col-md-3 col-form-label pt-0"> |               <div class="col"> | ||||||
|                 <span i18n>Full search links to</span> |                 <pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check> | ||||||
|               </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> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
| @@ -241,10 +229,26 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <h5 class="mt-3" i18n>Notes</h5> |             <h5 class="mt-3" i18n>Global search</h5> | ||||||
|             <div class="row mb-3"> |             <div class="row mb-3"> | ||||||
|               <div class="col"> |               <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"> | ||||||
|  |                 <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> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
| @@ -263,8 +267,8 @@ | |||||||
|         <div class="row mb-3"> |         <div class="row mb-3"> | ||||||
|           <div class="col"> |           <div class="col"> | ||||||
|             <p i18n> |             <p i18n> | ||||||
|               Settings apply to this user account for objects (Tags, Mail Rules, etc. but not documents) created via the web UI. |             Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI | ||||||
|             </p> |           </p> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="row mb-3"> |         <div class="row mb-3"> | ||||||
| @@ -303,7 +307,7 @@ | |||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="row"> |         <div class="row mb-3"> | ||||||
|           <div class="col-md-3 col-form-label pt-0"> |           <div class="col-md-3 col-form-label pt-0"> | ||||||
|             <span i18n>Default Edit Permissions</span> |             <span i18n>Default Edit Permissions</span> | ||||||
|           </div> |           </div> | ||||||
| @@ -342,7 +346,7 @@ | |||||||
|  |  | ||||||
|         <h5 i18n>Document processing</h5> |         <h5 i18n>Document processing</h5> | ||||||
|  |  | ||||||
|         <div class="row"> |         <div class="row mb-3"> | ||||||
|           <div class="col"> |           <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 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> |             <pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check> | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ | |||||||
|     } |     } | ||||||
|     <div class="scroll-list"> |     <div class="scroll-list"> | ||||||
|       @for (toast of toasts; track toast.id) { |       @for (toast of toasts; track toast.id) { | ||||||
|         <pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (closed)="toastService.closeToast(toast)"></pngx-toast> |         <pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (close)="toastService.closeToast(toast)"></pngx-toast> | ||||||
|       } |       } | ||||||
|       </div> |       </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -51,6 +51,6 @@ | |||||||
|             <p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="close.emit(toast); toast.action()">{{toast.actionName}}</button></p> |             <p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="close.emit(toast); toast.action()">{{toast.actionName}}</button></p> | ||||||
|             } |             } | ||||||
|         </div> |         </div> | ||||||
|         <button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="closed.emit(toast);"></button> |         <button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="close.emit(toast);"></button> | ||||||
|     </div> |     </div> | ||||||
| </ngb-toast> | </ngb-toast> | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ export class ToastComponent { | |||||||
|  |  | ||||||
|   @Output() hidden: EventEmitter<Toast> = new EventEmitter<Toast>() |   @Output() hidden: EventEmitter<Toast> = new EventEmitter<Toast>() | ||||||
|  |  | ||||||
|   @Output() closed: EventEmitter<Toast> = new EventEmitter<Toast>() |   @Output() close: EventEmitter<Toast> = new EventEmitter<Toast>() | ||||||
|  |  | ||||||
|   public copied: boolean = false |   public copied: boolean = false | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,3 @@ | |||||||
| @for (toast of toasts; track toast.id) { | @for (toast of toasts; track toast.id) { | ||||||
|   <pngx-toast [toast]="toast" [autohide]="true" (closed)="closeToast()"></pngx-toast> |   <pngx-toast [toast]="toast" [autohide]="true" (close)="closeToast()"></pngx-toast> | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ from documents.models import CustomField | |||||||
| from documents.models import CustomFieldInstance | from documents.models import CustomFieldInstance | ||||||
| from documents.models import Document | from documents.models import Document | ||||||
| from documents.models import DocumentType | from documents.models import DocumentType | ||||||
|  | from documents.models import FileInfo | ||||||
| from documents.models import StoragePath | from documents.models import StoragePath | ||||||
| from documents.models import Tag | from documents.models import Tag | ||||||
| from documents.models import WorkflowTrigger | from documents.models import WorkflowTrigger | ||||||
| @@ -704,6 +705,8 @@ class ConsumerPlugin( | |||||||
|     ) -> Document: |     ) -> Document: | ||||||
|         # If someone gave us the original filename, use it instead of doc. |         # If someone gave us the original filename, use it instead of doc. | ||||||
|  |  | ||||||
|  |         file_info = FileInfo.from_filename(self.filename) | ||||||
|  |  | ||||||
|         self.log.debug("Saving record to database") |         self.log.debug("Saving record to database") | ||||||
|  |  | ||||||
|         if self.metadata.created is not None: |         if self.metadata.created is not None: | ||||||
| @@ -711,6 +714,9 @@ class ConsumerPlugin( | |||||||
|             self.log.debug( |             self.log.debug( | ||||||
|                 f"Creation date from post_documents parameter: {create_date}", |                 f"Creation date from post_documents parameter: {create_date}", | ||||||
|             ) |             ) | ||||||
|  |         elif file_info.created is not None: | ||||||
|  |             create_date = file_info.created | ||||||
|  |             self.log.debug(f"Creation date from FileInfo: {create_date}") | ||||||
|         elif date is not None: |         elif date is not None: | ||||||
|             create_date = date |             create_date = date | ||||||
|             self.log.debug(f"Creation date from parse_date: {create_date}") |             self.log.debug(f"Creation date from parse_date: {create_date}") | ||||||
| @@ -723,11 +729,7 @@ class ConsumerPlugin( | |||||||
|  |  | ||||||
|         storage_type = Document.STORAGE_TYPE_UNENCRYPTED |         storage_type = Document.STORAGE_TYPE_UNENCRYPTED | ||||||
|  |  | ||||||
|         if self.metadata.filename: |         title = file_info.title | ||||||
|             title = Path(self.metadata.filename).stem |  | ||||||
|         else: |  | ||||||
|             title = self.input_doc.original_file.stem |  | ||||||
|  |  | ||||||
|         if self.metadata.title is not None: |         if self.metadata.title is not None: | ||||||
|             try: |             try: | ||||||
|                 title = self._parse_title_placeholders(self.metadata.title) |                 title = self._parse_title_placeholders(self.metadata.title) | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ from documents.models import CustomField | |||||||
| from documents.models import CustomFieldInstance | from documents.models import CustomFieldInstance | ||||||
| from documents.models import Document | from documents.models import Document | ||||||
| from documents.models import DocumentType | from documents.models import DocumentType | ||||||
|  | from documents.models import Log | ||||||
| from documents.models import PaperlessTask | from documents.models import PaperlessTask | ||||||
| from documents.models import ShareLink | from documents.models import ShareLink | ||||||
| from documents.models import StoragePath | from documents.models import StoragePath | ||||||
| @@ -760,6 +761,12 @@ class DocumentFilterSet(FilterSet): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LogFilterSet(FilterSet): | ||||||
|  |     class Meta: | ||||||
|  |         model = Log | ||||||
|  |         fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS} | ||||||
|  |  | ||||||
|  |  | ||||||
| class ShareLinkFilterSet(FilterSet): | class ShareLinkFilterSet(FilterSet): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = ShareLink |         model = ShareLink | ||||||
|   | |||||||
| @@ -1,15 +0,0 @@ | |||||||
| # Generated by Django 5.1.6 on 2025-02-28 15:19 |  | ||||||
|  |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.DeleteModel( |  | ||||||
|             name="Log", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -6,7 +6,7 @@ from django.db import models | |||||||
| 
 | 
 | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|     dependencies = [ |     dependencies = [ | ||||||
|         ("documents", "1064_delete_log"), |         ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"), | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     operations = [ |     operations = [ | ||||||
| @@ -18,7 +18,7 @@ class Migration(migrations.Migration): | |||||||
|                 help_text="Optional values to assign to the custom fields.", |                 help_text="Optional values to assign to the custom fields.", | ||||||
|                 null=True, |                 null=True, | ||||||
|                 verbose_name="custom field values", |                 verbose_name="custom field values", | ||||||
|                 default=dict, |                 default={}, | ||||||
|             ), |             ), | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
| @@ -1,7 +1,12 @@ | |||||||
| import datetime | import datetime | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | from collections import OrderedDict | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Final | from typing import Final | ||||||
|  |  | ||||||
|  | import dateutil.parser | ||||||
| import pathvalidate | import pathvalidate | ||||||
| from celery import states | from celery import states | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @@ -374,6 +379,36 @@ class Document(SoftDeleteModel, ModelWithOwner): | |||||||
|         return timezone.localdate(self.created) |         return timezone.localdate(self.created) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Log(models.Model): | ||||||
|  |     LEVELS = ( | ||||||
|  |         (logging.DEBUG, _("debug")), | ||||||
|  |         (logging.INFO, _("information")), | ||||||
|  |         (logging.WARNING, _("warning")), | ||||||
|  |         (logging.ERROR, _("error")), | ||||||
|  |         (logging.CRITICAL, _("critical")), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     group = models.UUIDField(_("group"), blank=True, null=True) | ||||||
|  |  | ||||||
|  |     message = models.TextField(_("message")) | ||||||
|  |  | ||||||
|  |     level = models.PositiveIntegerField( | ||||||
|  |         _("level"), | ||||||
|  |         choices=LEVELS, | ||||||
|  |         default=logging.INFO, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     created = models.DateTimeField(_("created"), auto_now_add=True) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         ordering = ("-created",) | ||||||
|  |         verbose_name = _("log") | ||||||
|  |         verbose_name_plural = _("logs") | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.message | ||||||
|  |  | ||||||
|  |  | ||||||
| class SavedView(ModelWithOwner): | class SavedView(ModelWithOwner): | ||||||
|     class DisplayMode(models.TextChoices): |     class DisplayMode(models.TextChoices): | ||||||
|         TABLE = ("table", _("Table")) |         TABLE = ("table", _("Table")) | ||||||
| @@ -513,6 +548,91 @@ class SavedViewFilterRule(models.Model): | |||||||
|         return f"SavedViewFilterRule: {self.rule_type} : {self.value}" |         return f"SavedViewFilterRule: {self.rule_type} : {self.value}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # TODO: why is this in the models file? | ||||||
|  | # TODO: how about, what is this and where is it documented? | ||||||
|  | # It appears to parsing JSON from an environment variable to get a title and date from | ||||||
|  | # the filename, if possible, as a higher priority than either document filename or | ||||||
|  | # content parsing | ||||||
|  | class FileInfo: | ||||||
|  |     REGEXES = OrderedDict( | ||||||
|  |         [ | ||||||
|  |             ( | ||||||
|  |                 "created-title", | ||||||
|  |                 re.compile( | ||||||
|  |                     r"^(?P<created>\d{8}(\d{6})?Z) - (?P<title>.*)$", | ||||||
|  |                     flags=re.IGNORECASE, | ||||||
|  |                 ), | ||||||
|  |             ), | ||||||
|  |             ("title", re.compile(r"(?P<title>.*)$", flags=re.IGNORECASE)), | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         created=None, | ||||||
|  |         correspondent=None, | ||||||
|  |         title=None, | ||||||
|  |         tags=(), | ||||||
|  |         extension=None, | ||||||
|  |     ): | ||||||
|  |         self.created = created | ||||||
|  |         self.title = title | ||||||
|  |         self.extension = extension | ||||||
|  |         self.correspondent = correspondent | ||||||
|  |         self.tags = tags | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _get_created(cls, created): | ||||||
|  |         try: | ||||||
|  |             return dateutil.parser.parse(f"{created[:-1]:0<14}Z") | ||||||
|  |         except ValueError: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _get_title(cls, title): | ||||||
|  |         return title | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _mangle_property(cls, properties, name): | ||||||
|  |         if name in properties: | ||||||
|  |             properties[name] = getattr(cls, f"_get_{name}")(properties[name]) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def from_filename(cls, filename) -> "FileInfo": | ||||||
|  |         # Mutate filename in-place before parsing its components | ||||||
|  |         # by applying at most one of the configured transformations. | ||||||
|  |         for pattern, repl in settings.FILENAME_PARSE_TRANSFORMS: | ||||||
|  |             (filename, count) = pattern.subn(repl, filename) | ||||||
|  |             if count: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         # do this after the transforms so that the transforms can do whatever | ||||||
|  |         # with the file extension. | ||||||
|  |         filename_no_ext = os.path.splitext(filename)[0] | ||||||
|  |  | ||||||
|  |         if filename_no_ext == filename and filename.startswith("."): | ||||||
|  |             # This is a very special case where there is no text before the | ||||||
|  |             # file type. | ||||||
|  |             # TODO: this should be handled better. The ext is not removed | ||||||
|  |             #  because usually, files like '.pdf' are just hidden files | ||||||
|  |             #  with the name pdf, but in our case, its more likely that | ||||||
|  |             #  there's just no name to begin with. | ||||||
|  |             filename = "" | ||||||
|  |             # This isn't too bad either, since we'll just not match anything | ||||||
|  |             # and return an empty title. TODO: actually, this is kinda bad. | ||||||
|  |         else: | ||||||
|  |             filename = filename_no_ext | ||||||
|  |  | ||||||
|  |         # Parse filename components. | ||||||
|  |         for regex in cls.REGEXES.values(): | ||||||
|  |             m = regex.match(filename) | ||||||
|  |             if m: | ||||||
|  |                 properties = m.groupdict() | ||||||
|  |                 cls._mangle_property(properties, "created") | ||||||
|  |                 cls._mangle_property(properties, "title") | ||||||
|  |                 return cls(**properties) | ||||||
|  |  | ||||||
|  |  | ||||||
| # Extending User Model Using a One-To-One Link | # Extending User Model Using a One-To-One Link | ||||||
| class UiSettings(models.Model): | class UiSettings(models.Model): | ||||||
|     user = models.OneToOneField( |     user = models.OneToOneField( | ||||||
| @@ -1278,7 +1398,7 @@ class WorkflowAction(models.Model): | |||||||
|         help_text=_( |         help_text=_( | ||||||
|             "Optional values to assign to the custom fields.", |             "Optional values to assign to the custom fields.", | ||||||
|         ), |         ), | ||||||
|         default=dict, |         default={}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     remove_tags = models.ManyToManyField( |     remove_tags = models.ManyToManyField( | ||||||
|   | |||||||
| @@ -632,7 +632,7 @@ def send_webhook( | |||||||
|         else: |         else: | ||||||
|             httpx.post( |             httpx.post( | ||||||
|                 url, |                 url, | ||||||
|                 content=data, |                 data=data, | ||||||
|                 files=files, |                 files=files, | ||||||
|                 headers=headers, |                 headers=headers, | ||||||
|             ).raise_for_status() |             ).raise_for_status() | ||||||
| @@ -1019,37 +1019,29 @@ def run_workflows( | |||||||
|             added = timezone.localtime(timezone.now()) |             added = timezone.localtime(timezone.now()) | ||||||
|             created = timezone.localtime(overrides.created) |             created = timezone.localtime(overrides.created) | ||||||
|  |  | ||||||
|         subject = ( |         subject = parse_w_workflow_placeholders( | ||||||
|             parse_w_workflow_placeholders( |             action.email.subject, | ||||||
|                 action.email.subject, |             correspondent, | ||||||
|                 correspondent, |             document_type, | ||||||
|                 document_type, |             owner_username, | ||||||
|                 owner_username, |             added, | ||||||
|                 added, |             filename, | ||||||
|                 filename, |             current_filename, | ||||||
|                 current_filename, |             created, | ||||||
|                 created, |             title, | ||||||
|                 title, |             doc_url, | ||||||
|                 doc_url, |  | ||||||
|             ) |  | ||||||
|             if action.email.subject |  | ||||||
|             else "" |  | ||||||
|         ) |         ) | ||||||
|         body = ( |         body = parse_w_workflow_placeholders( | ||||||
|             parse_w_workflow_placeholders( |             action.email.body, | ||||||
|                 action.email.body, |             correspondent, | ||||||
|                 correspondent, |             document_type, | ||||||
|                 document_type, |             owner_username, | ||||||
|                 owner_username, |             added, | ||||||
|                 added, |             filename, | ||||||
|                 filename, |             current_filename, | ||||||
|                 current_filename, |             created, | ||||||
|                 created, |             title, | ||||||
|                 title, |             doc_url, | ||||||
|                 doc_url, |  | ||||||
|             ) |  | ||||||
|             if action.email.body |  | ||||||
|             else "" |  | ||||||
|         ) |         ) | ||||||
|         try: |         try: | ||||||
|             n_messages = send_email( |             n_messages = send_email( | ||||||
| @@ -1130,7 +1122,7 @@ def run_workflows( | |||||||
|                             f"Error occurred parsing webhook params: {e}", |                             f"Error occurred parsing webhook params: {e}", | ||||||
|                             extra={"group": logging_group}, |                             extra={"group": logging_group}, | ||||||
|                         ) |                         ) | ||||||
|             elif action.webhook.body: |             else: | ||||||
|                 data = parse_w_workflow_placeholders( |                 data = parse_w_workflow_placeholders( | ||||||
|                     action.webhook.body, |                     action.webhook.body, | ||||||
|                     correspondent, |                     correspondent, | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ from pathlib import PurePath | |||||||
| import pathvalidate | import pathvalidate | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.dateparse import parse_date | from django.utils.dateparse import parse_date | ||||||
| from django.utils.text import slugify as django_slugify |  | ||||||
| from jinja2 import StrictUndefined | from jinja2 import StrictUndefined | ||||||
| from jinja2 import Template | from jinja2 import Template | ||||||
| from jinja2 import TemplateSyntaxError | from jinja2 import TemplateSyntaxError | ||||||
| @@ -101,8 +100,6 @@ def format_datetime(value: str | datetime, format: str) -> str: | |||||||
|  |  | ||||||
| _template_environment.filters["datetime"] = format_datetime | _template_environment.filters["datetime"] = format_datetime | ||||||
|  |  | ||||||
| _template_environment.filters["slugify"] = django_slugify |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_dummy_document(): | def create_dummy_document(): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
| import datetime | import datetime | ||||||
| import os | import os | ||||||
|  | import re | ||||||
| import shutil | import shutil | ||||||
| import stat | import stat | ||||||
| import tempfile | import tempfile | ||||||
| import zoneinfo | import zoneinfo | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from unittest import TestCase as UnittestTestCase | ||||||
| from unittest import mock | from unittest import mock | ||||||
| from unittest.mock import MagicMock | from unittest.mock import MagicMock | ||||||
|  |  | ||||||
| @@ -24,6 +26,7 @@ from documents.models import Correspondent | |||||||
| from documents.models import CustomField | from documents.models import CustomField | ||||||
| from documents.models import Document | from documents.models import Document | ||||||
| from documents.models import DocumentType | from documents.models import DocumentType | ||||||
|  | from documents.models import FileInfo | ||||||
| from documents.models import StoragePath | from documents.models import StoragePath | ||||||
| from documents.models import Tag | from documents.models import Tag | ||||||
| from documents.parsers import DocumentParser | from documents.parsers import DocumentParser | ||||||
| @@ -37,6 +40,143 @@ from paperless_mail.models import MailRule | |||||||
| from paperless_mail.parsers import MailDocumentParser | from paperless_mail.parsers import MailDocumentParser | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestAttributes(UnittestTestCase): | ||||||
|  |     TAGS = ("tag1", "tag2", "tag3") | ||||||
|  |  | ||||||
|  |     def _test_guess_attributes_from_name(self, filename, sender, title, tags): | ||||||
|  |         file_info = FileInfo.from_filename(filename) | ||||||
|  |  | ||||||
|  |         if sender: | ||||||
|  |             self.assertEqual(file_info.correspondent.name, sender, filename) | ||||||
|  |         else: | ||||||
|  |             self.assertIsNone(file_info.correspondent, filename) | ||||||
|  |  | ||||||
|  |         self.assertEqual(file_info.title, title, filename) | ||||||
|  |  | ||||||
|  |         self.assertEqual(tuple(t.name for t in file_info.tags), tags, filename) | ||||||
|  |  | ||||||
|  |     def test_guess_attributes_from_name_when_title_starts_with_dash(self): | ||||||
|  |         self._test_guess_attributes_from_name( | ||||||
|  |             "- weird but should not break.pdf", | ||||||
|  |             None, | ||||||
|  |             "- weird but should not break", | ||||||
|  |             (), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_guess_attributes_from_name_when_title_ends_with_dash(self): | ||||||
|  |         self._test_guess_attributes_from_name( | ||||||
|  |             "weird but should not break -.pdf", | ||||||
|  |             None, | ||||||
|  |             "weird but should not break -", | ||||||
|  |             (), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestFieldPermutations(TestCase): | ||||||
|  |     valid_dates = ( | ||||||
|  |         "20150102030405Z", | ||||||
|  |         "20150102Z", | ||||||
|  |     ) | ||||||
|  |     valid_correspondents = ["timmy", "Dr. McWheelie", "Dash Gor-don", "o Θεpμaoτής", ""] | ||||||
|  |     valid_titles = ["title", "Title w Spaces", "Title a-dash", "Tίτλoς", ""] | ||||||
|  |     valid_tags = ["tag", "tig,tag", "tag1,tag2,tag-3"] | ||||||
|  |  | ||||||
|  |     def _test_guessed_attributes( | ||||||
|  |         self, | ||||||
|  |         filename, | ||||||
|  |         created=None, | ||||||
|  |         correspondent=None, | ||||||
|  |         title=None, | ||||||
|  |         tags=None, | ||||||
|  |     ): | ||||||
|  |         info = FileInfo.from_filename(filename) | ||||||
|  |  | ||||||
|  |         # Created | ||||||
|  |         if created is None: | ||||||
|  |             self.assertIsNone(info.created, filename) | ||||||
|  |         else: | ||||||
|  |             self.assertEqual(info.created.year, int(created[:4]), filename) | ||||||
|  |             self.assertEqual(info.created.month, int(created[4:6]), filename) | ||||||
|  |             self.assertEqual(info.created.day, int(created[6:8]), filename) | ||||||
|  |  | ||||||
|  |         # Correspondent | ||||||
|  |         if correspondent: | ||||||
|  |             self.assertEqual(info.correspondent.name, correspondent, filename) | ||||||
|  |         else: | ||||||
|  |             self.assertEqual(info.correspondent, None, filename) | ||||||
|  |  | ||||||
|  |         # Title | ||||||
|  |         self.assertEqual(info.title, title, filename) | ||||||
|  |  | ||||||
|  |         # Tags | ||||||
|  |         if tags is None: | ||||||
|  |             self.assertEqual(info.tags, (), filename) | ||||||
|  |         else: | ||||||
|  |             self.assertEqual([t.name for t in info.tags], tags.split(","), filename) | ||||||
|  |  | ||||||
|  |     def test_just_title(self): | ||||||
|  |         template = "{title}.pdf" | ||||||
|  |         for title in self.valid_titles: | ||||||
|  |             spec = dict(title=title) | ||||||
|  |             filename = template.format(**spec) | ||||||
|  |             self._test_guessed_attributes(filename, **spec) | ||||||
|  |  | ||||||
|  |     def test_created_and_title(self): | ||||||
|  |         template = "{created} - {title}.pdf" | ||||||
|  |  | ||||||
|  |         for created in self.valid_dates: | ||||||
|  |             for title in self.valid_titles: | ||||||
|  |                 spec = {"created": created, "title": title} | ||||||
|  |                 self._test_guessed_attributes(template.format(**spec), **spec) | ||||||
|  |  | ||||||
|  |     def test_invalid_date_format(self): | ||||||
|  |         info = FileInfo.from_filename("06112017Z - title.pdf") | ||||||
|  |         self.assertEqual(info.title, "title") | ||||||
|  |         self.assertIsNone(info.created) | ||||||
|  |  | ||||||
|  |     def test_filename_parse_transforms(self): | ||||||
|  |         filename = "tag1,tag2_20190908_180610_0001.pdf" | ||||||
|  |         all_patt = re.compile("^.*$") | ||||||
|  |         none_patt = re.compile("$a") | ||||||
|  |         re.compile("^([a-z0-9,]+)_(\\d{8})_(\\d{6})_([0-9]+)\\.") | ||||||
|  |  | ||||||
|  |         # No transformations configured (= default) | ||||||
|  |         info = FileInfo.from_filename(filename) | ||||||
|  |         self.assertEqual(info.title, "tag1,tag2_20190908_180610_0001") | ||||||
|  |         self.assertEqual(info.tags, ()) | ||||||
|  |         self.assertIsNone(info.created) | ||||||
|  |  | ||||||
|  |         # Pattern doesn't match (filename unaltered) | ||||||
|  |         with self.settings(FILENAME_PARSE_TRANSFORMS=[(none_patt, "none.gif")]): | ||||||
|  |             info = FileInfo.from_filename(filename) | ||||||
|  |             self.assertEqual(info.title, "tag1,tag2_20190908_180610_0001") | ||||||
|  |  | ||||||
|  |         # Simple transformation (match all) | ||||||
|  |         with self.settings(FILENAME_PARSE_TRANSFORMS=[(all_patt, "all.gif")]): | ||||||
|  |             info = FileInfo.from_filename(filename) | ||||||
|  |             self.assertEqual(info.title, "all") | ||||||
|  |  | ||||||
|  |         # Multiple transformations configured (first pattern matches) | ||||||
|  |         with self.settings( | ||||||
|  |             FILENAME_PARSE_TRANSFORMS=[ | ||||||
|  |                 (all_patt, "all.gif"), | ||||||
|  |                 (all_patt, "anotherall.gif"), | ||||||
|  |             ], | ||||||
|  |         ): | ||||||
|  |             info = FileInfo.from_filename(filename) | ||||||
|  |             self.assertEqual(info.title, "all") | ||||||
|  |  | ||||||
|  |         # Multiple transformations configured (second pattern matches) | ||||||
|  |         with self.settings( | ||||||
|  |             FILENAME_PARSE_TRANSFORMS=[ | ||||||
|  |                 (none_patt, "none.gif"), | ||||||
|  |                 (all_patt, "anotherall.gif"), | ||||||
|  |             ], | ||||||
|  |         ): | ||||||
|  |             info = FileInfo.from_filename(filename) | ||||||
|  |             self.assertEqual(info.title, "anotherall") | ||||||
|  |  | ||||||
|  |  | ||||||
| class _BaseTestParser(DocumentParser): | class _BaseTestParser(DocumentParser): | ||||||
|     def get_settings(self): |     def get_settings(self): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -1517,63 +1517,3 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): | |||||||
|                 generate_filename(doc_a), |                 generate_filename(doc_a), | ||||||
|                 "2024-10-01/Some Title.pdf", |                 "2024-10-01/Some Title.pdf", | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     def test_slugify_filter(self): |  | ||||||
|         """ |  | ||||||
|         GIVEN: |  | ||||||
|             - Filename format with slugify filter |  | ||||||
|         WHEN: |  | ||||||
|             - Filepath for a document with this format is called |  | ||||||
|         THEN: |  | ||||||
|             - The slugify filter properly converts strings to URL-friendly slugs |  | ||||||
|         """ |  | ||||||
|         doc = Document.objects.create( |  | ||||||
|             title="Some Title! With @ Special # Characters", |  | ||||||
|             created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), |  | ||||||
|             added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), |  | ||||||
|             mime_type="application/pdf", |  | ||||||
|             pk=2, |  | ||||||
|             checksum="2", |  | ||||||
|             archive_serial_number=25, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         with override_settings( |  | ||||||
|             FILENAME_FORMAT="{{ title | slugify }}", |  | ||||||
|         ): |  | ||||||
|             self.assertEqual( |  | ||||||
|                 generate_filename(doc), |  | ||||||
|                 "some-title-with-special-characters.pdf", |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         # Test with correspondent name containing spaces and special chars |  | ||||||
|         doc.correspondent = Correspondent.objects.create( |  | ||||||
|             name="John's @ Office / Workplace", |  | ||||||
|         ) |  | ||||||
|         doc.save() |  | ||||||
|  |  | ||||||
|         with override_settings( |  | ||||||
|             FILENAME_FORMAT="{{ correspondent | slugify }}/{{ title | slugify }}", |  | ||||||
|         ): |  | ||||||
|             self.assertEqual( |  | ||||||
|                 generate_filename(doc), |  | ||||||
|                 "johns-office-workplace/some-title-with-special-characters.pdf", |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         # Test with custom fields |  | ||||||
|         cf = CustomField.objects.create( |  | ||||||
|             name="Location", |  | ||||||
|             data_type=CustomField.FieldDataType.STRING, |  | ||||||
|         ) |  | ||||||
|         CustomFieldInstance.objects.create( |  | ||||||
|             document=doc, |  | ||||||
|             field=cf, |  | ||||||
|             value_text="Brussels @ Belgium!", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         with override_settings( |  | ||||||
|             FILENAME_FORMAT="{{ custom_fields | get_cf_value('Location') | slugify }}/{{ title | slugify }}", |  | ||||||
|         ): |  | ||||||
|             self.assertEqual( |  | ||||||
|                 generate_filename(doc), |  | ||||||
|                 "brussels-belgium/some-title-with-special-characters.pdf", |  | ||||||
|             ) |  | ||||||
|   | |||||||
| @@ -2615,7 +2615,7 @@ class TestWorkflows( | |||||||
|  |  | ||||||
|             mock_post.assert_called_once_with( |             mock_post.assert_called_once_with( | ||||||
|                 "http://paperless-ngx.com", |                 "http://paperless-ngx.com", | ||||||
|                 content="Test message", |                 data="Test message", | ||||||
|                 headers={}, |                 headers={}, | ||||||
|                 files=None, |                 files=None, | ||||||
|             ) |             ) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import json | |||||||
| import math | import math | ||||||
| import multiprocessing | import multiprocessing | ||||||
| import os | import os | ||||||
|  | import re | ||||||
| import tempfile | import tempfile | ||||||
| from os import PathLike | from os import PathLike | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| @@ -1088,6 +1089,11 @@ FILENAME_DATE_ORDER = os.getenv("PAPERLESS_FILENAME_DATE_ORDER") | |||||||
| # fewer dates shown. | # fewer dates shown. | ||||||
| NUMBER_OF_SUGGESTED_DATES = __get_int("PAPERLESS_NUMBER_OF_SUGGESTED_DATES", 3) | NUMBER_OF_SUGGESTED_DATES = __get_int("PAPERLESS_NUMBER_OF_SUGGESTED_DATES", 3) | ||||||
|  |  | ||||||
|  | # Transformations applied before filename parsing | ||||||
|  | FILENAME_PARSE_TRANSFORMS = [] | ||||||
|  | for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")): | ||||||
|  |     FILENAME_PARSE_TRANSFORMS.append((re.compile(t["pattern"]), t["repl"])) | ||||||
|  |  | ||||||
| # Specify the filename format for out files | # Specify the filename format for out files | ||||||
| FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT") | FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT") | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								src/setup.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/setup.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | [tool:pytest] | ||||||
|  | DJANGO_SETTINGS_MODULE = paperless.settings | ||||||
|  | addopts = --pythonwarnings=all --cov --cov-report=html --cov-report=xml --numprocesses auto --maxprocesses=16 --quiet --durations=50 | ||||||
|  | env = | ||||||
|  |     PAPERLESS_DISABLE_DBHANDLER=true | ||||||
|  |     PAPERLESS_CACHE_BACKEND=django.core.cache.backends.locmem.LocMemCache | ||||||
|  | norecursedirs = locale/* | ||||||
|  |  | ||||||
|  | [coverage:run] | ||||||
|  | source = | ||||||
|  |     ./ | ||||||
|  | omit = | ||||||
|  |     */tests/* | ||||||
|  |     manage.py | ||||||
|  |     paperless/workers.py | ||||||
|  |     paperless/wsgi.py | ||||||
|  |     paperless/auth.py | ||||||
|  |  | ||||||
|  | [coverage:report] | ||||||
|  | exclude_also = | ||||||
|  |     if settings.AUDIT_LOG_ENABLED: | ||||||
|  |     if AUDIT_LOG_ENABLED: | ||||||
|  |     if TYPE_CHECKING: | ||||||
|  |  | ||||||
|  | [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 | ||||||
|  |  | ||||||
|  | [mypy.plugins.django-stubs] | ||||||
|  | django_settings_module = "paperless.settings" | ||||||
| @@ -6,7 +6,7 @@ if __name__ == "__main__": | |||||||
|  |  | ||||||
|     Granian( |     Granian( | ||||||
|         "paperless.asgi:application", |         "paperless.asgi:application", | ||||||
|         interface=Interfaces.ASGINL, |         interface=Interfaces.ASGI, | ||||||
|         address=os.getenv("GRANIAN_HOST") or os.getenv("PAPERLESS_BIND_ADDR", "::"), |         address=os.getenv("GRANIAN_HOST") or os.getenv("PAPERLESS_BIND_ADDR", "::"), | ||||||
|         port=int(os.getenv("GRANIAN_PORT") or os.getenv("PAPERLESS_PORT") or 8000), |         port=int(os.getenv("GRANIAN_PORT") or os.getenv("PAPERLESS_PORT") or 8000), | ||||||
|         workers=int( |         workers=int( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user