Compare commits

..
Author SHA1 Message Date
shamoon 337092f345 Fix: handle versioned PaperlessTasks response 2025-03-08 16:38:12 -08:00
133 changed files with 77440 additions and 101068 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ RUN --mount=type=cache,target=/root/.cache/uv,id=pip-cache \
&& apt-get install --yes --quiet ${BUILD_PACKAGES}
RUN set -eux \
&& npm update -g pnpm
&& npm update npm -g
# add users, setup scripts
# Mount the compiled frontend to expected location
+4 -4
View File
@@ -33,7 +33,7 @@
"label": "Start: Frontend Angular",
"description": "Start the Frontend Angular Dev Server",
"type": "shell",
"command": "pnpm start",
"command": "npm start",
"isBackground": true,
"options": {
"cwd": "${workspaceFolder}/src-ui"
@@ -173,8 +173,8 @@
},
{
"label": "Maintenance: Install Frontend Dependencies",
"description": "Install frontend (pnpm) dependencies",
"type": "pnpm",
"description": "Install frontend (npm) dependencies",
"type": "npm",
"script": "install",
"path": "src-ui",
"group": "clean",
@@ -185,7 +185,7 @@
"description": "Clean install frontend dependencies and build the frontend for production",
"label": "Maintenance: Compile frontend for production",
"type": "shell",
"command": "pnpm install && ./node_modules/.bin/ng build --configuration production",
"command": "npm ci && ./node_modules/.bin/ng build --configuration production",
"group": "none",
"presentation": {
"echo": true,
+3 -51
View File
@@ -1,15 +1,14 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem
version: 2
# Required for uv support for now
enable-beta-ecosystems: true
updates:
# Enable version updates for pnpm
# Enable version updates for npm
- package-ecosystem: "npm"
target-branch: "dev"
# Look for `pnpm-lock.yaml` file in the `/src-ui` directory
# Look for `package.json` and `lock` files in the `/src-ui` directory
directory: "/src-ui"
open-pull-requests-limit: 10
schedule:
@@ -90,50 +89,3 @@ updates:
- "major"
- "minor"
- "patch"
# Update Dockerfile in root directory
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
reviewers:
- "paperless-ngx/ci-cd"
labels:
- "ci-cd"
- "dependencies"
commit-message:
prefix: "docker"
include: "scope"
# Update Docker Compose files in docker/compose directory
- package-ecosystem: "docker-compose"
directory: "/docker/compose/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
reviewers:
- "paperless-ngx/ci-cd"
labels:
- "ci-cd"
- "dependencies"
commit-message:
prefix: "docker-compose"
include: "scope"
groups:
# Individual groups for each image
gotenberg:
patterns:
- "docker.io/gotenberg/gotenberg*"
tika:
patterns:
- "docker.io/apache/tika*"
redis:
patterns:
- "docker.io/library/redis*"
mariadb:
patterns:
- "docker.io/library/mariadb*"
postgres:
patterns:
- "docker.io/library/postgres*"
+21 -33
View File
@@ -190,33 +190,29 @@ jobs:
- pre-commit
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.npm
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
-
name: Install dependencies
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
run: cd src-ui && pnpm install
run: cd src-ui && npm ci
-
name: Install Playwright
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
run: cd src-ui && pnpm playwright install --with-deps
run: cd src-ui && npx playwright install --with-deps
tests-frontend:
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
@@ -231,36 +227,32 @@ jobs:
shard-count: [4]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.npm
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
run: cd src-ui && npm link @angular/cli
-
name: Linting checks
run: cd src-ui && pnpm run lint
run: cd src-ui && npm run lint
-
name: Run Jest unit tests
run: cd src-ui && pnpm 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: Run Playwright e2e tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
-
name: Upload frontend test results to Codecov
uses: codecov/test-results-action@v1
@@ -284,35 +276,30 @@ jobs:
- tests-frontend
steps:
- uses: actions/checkout@v4
-
name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json'
-
name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.npm
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
-
name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
run: cd src-ui && npm link @angular/cli
-
name: Build frontend and upload analysis
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: cd src-ui && pnpm run build --configuration=production
run: cd src-ui && ng build --configuration=production
build-docker-image:
name: Build Docker image for ${{ github.ref_name }}
@@ -521,7 +508,8 @@ jobs:
requirements.txt \
LICENSE \
README.md \
paperless.conf.example
paperless.conf.example \
webserver.py
do
cp --verbose ${file_name} dist/paperless-ngx/
done
+1 -1
View File
@@ -32,7 +32,7 @@ repos:
rev: v2.4.0
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude_types:
- pofile
- json
+9 -6
View File
@@ -4,17 +4,15 @@
# Stage: compile-frontend
# Purpose: Compiles the frontend
# Notes:
# - Does PNPM stuff with Typescript and such
# - Does NPM stuff with Typescript and such
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim AS compile-frontend
COPY ./src-ui /src/src-ui
WORKDIR /src/src-ui
RUN set -eux \
&& npm update -g pnpm \
&& npm install -g corepack@latest \
&& corepack enable \
&& pnpm install
&& npm update npm -g \
&& npm ci
ARG PNGX_TAG_VERSION=
# Add the tag to the environment file if its a tagged dev build
@@ -32,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.6.5-python3.12-bookworm-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.6.3-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6
@@ -192,6 +190,11 @@ RUN set -eux \
&& rm --force --verbose *.deb \
&& rm --recursive --force --verbose /var/lib/apt/lists/*
# Copy webserver config
# Changes very infrequently
WORKDIR /usr/src/paperless/
COPY --chown=1000:1000 webserver.py /usr/src/paperless/webserver.py
WORKDIR /usr/src/paperless/src/
# Python dependencies
+1 -1
View File
@@ -38,7 +38,7 @@ services:
- redisdata:/data
db:
image: docker.io/library/postgres:17
image: docker.io/library/postgres:16
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
@@ -38,7 +38,7 @@ services:
- redisdata:/data
db:
image: docker.io/library/postgres:17
image: docker.io/library/postgres:16
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
+1 -1
View File
@@ -34,7 +34,7 @@ services:
- redisdata:/data
db:
image: docker.io/library/postgres:17
image: docker.io/library/postgres:16
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
@@ -3,18 +3,8 @@
cd ${PAPERLESS_SRC_DIR}
# Translate between things, preferring GRANIAN_
export GRANIAN_HOST=${GRANIAN_HOST:-${PAPERLESS_BIND_ADDR:-"::"}}
export GRANIAN_PORT=${GRANIAN_PORT:-${PAPERLESS_PORT:-8000}}
export GRANIAN_WORKERS=${GRANIAN_WORKERS:-${PAPERLESS_WEBSERVER_WORKERS:-1}}
# Only set GRANIAN_URL_PATH_PREFIX if PAPERLESS_FORCE_SCRIPT_NAME is set
if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
fi
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec granian --interface asginl --ws "paperless.asgi:application"
exec python3 /usr/src/paperless/webserver.py
else
exec s6-setuidgid paperless granian --interface asginl --ws "paperless.asgi:application"
exec s6-setuidgid paperless python3 /usr/src/paperless/webserver.py
fi
+8
View File
@@ -413,3 +413,11 @@ Initial API version.
list of strings. When creating or updating a custom field value of a
document for a select type custom field, the value should be the `id` of
the option whereas previously was the index of the option.
#### Version 8
- PaperlessTask objects now have a `task_name` field which replaces the old
`type` field. The `type` field is now used to represent the way the task
was created. Additionally, the tasks endpoint now returns different types
of tasks other than simply 'file' tasks. See the API schema for more
information.
+5 -5
View File
@@ -140,7 +140,7 @@ To build the front end once use this command:
```bash
# src-ui/
$ pnpm install
$ npm install
$ ng build --configuration production
```
@@ -176,7 +176,7 @@ To add a new development package `uv add --dev <package>`
## Front end development
The front end is built using AngularJS. In order to get started, you need Node.js (version 14.15+) and
`pnpm`.
`npm`.
!!! note
@@ -185,7 +185,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
1. Install the Angular CLI. You might need sudo privileges to perform this command:
```bash
pnpm install -g @angular/cli
npm install -g @angular/cli
```
2. Make sure that it's on your path.
@@ -193,7 +193,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
3. Install all necessary modules:
```bash
pnpm install
npm install
```
4. You can launch a development server by running:
@@ -207,7 +207,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
restart it.
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts and CORS are in place so that the front end behaves exactly as in production.
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
### Testing and code style
+1 -1
View File
@@ -837,7 +837,7 @@ Paperless-ngx consists of the following components:
```shell-session
cd /path/to/paperless/src/
granian --interface asginl --ws "paperless.asgi:application"
python3 webserver.py
```
or by any other means such as Apache `mod_wsgi`.
+8 -8
View File
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "2.15.0"
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"
@@ -37,7 +37,7 @@ dependencies = [
"djangorestframework~=3.15",
"djangorestframework-guardian~=0.3.0",
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2025.3.1",
"drf-spectacular-sidecar~=2025.2.1",
"drf-writable-nested~=0.7.1",
"filelock~=3.17.0",
"flower~=2.0.1",
@@ -48,7 +48,7 @@ dependencies = [
"jinja2~=3.1.5",
"langdetect~=1.0.9",
"nltk~=3.9.1",
"ocrmypdf~=16.10.0",
"ocrmypdf~=16.9.0",
"pathvalidate~=3.2.3",
"pdf2image~=1.17.0",
"python-dateutil~=2.9.0",
@@ -73,12 +73,12 @@ optional-dependencies.mariadb = [
"mysqlclient~=2.2.7",
]
optional-dependencies.postgres = [
"psycopg[c]==3.2.5",
"psycopg[c]==3.2.4",
# Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.2.5",
"psycopg-c==3.2.4",
]
optional-dependencies.webserver = [
"granian~=2.0.1",
"granian~=1.7.6",
]
[dependency-groups]
@@ -343,8 +343,8 @@ environments = [
[tool.uv.sources]
# Markers are chosen to select these almost exclusively when building the Docker image
psycopg-c = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
{ 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'" },
+1 -15
View File
@@ -9,21 +9,7 @@ Requires=redis.service
User=paperless
Group=paperless
WorkingDirectory=/opt/paperless/src
Environment=GRANIAN_HOST=::
Environment=GRANIAN_PORT=8000
Environment=GRANIAN_WORKERS=1
ExecStart=/bin/sh -c '\
# Host: GRANIAN_HOST -> PAPERLESS_BIND_ADDR -> default \
[ -n "$PAPERLESS_BIND_ADDR" ] && export GRANIAN_HOST=$PAPERLESS_BIND_ADDR; \
# Port: GRANIAN_PORT -> PAPERLESS_PORT -> default \
[ -n "$PAPERLESS_PORT" ] && export GRANIAN_PORT=$PAPERLESS_PORT; \
# Workers: GRANIAN_WORKERS -> PAPERLESS_WEBSERVER_WORKERS -> default \
[ -n "$PAPERLESS_WEBSERVER_WORKERS" ] && export GRANIAN_WORKERS=$PAPERLESS_WEBSERVER_WORKERS; \
# URL path prefix: only set if PAPERLESS_FORCE_SCRIPT_NAME exists \
[ -n "$PAPERLESS_FORCE_SCRIPT_NAME" ] && export GRANIAN_URL_PATH_PREFIX=$PAPERLESS_FORCE_SCRIPT_NAME; \
exec granian --interface asginl --ws "paperless.asgi:application"'
ExecStart=python3 webserver.py
[Install]
WantedBy=multi-user.target
-1
View File
@@ -1 +0,0 @@
shamefully-hoist=true
+1 -2
View File
@@ -178,8 +178,7 @@
"schematicCollections": [
"@angular-eslint/schematics"
],
"analytics": false,
"packageManager": "pnpm"
"analytics": false
},
"schematics": {
"@angular-eslint/schematics:application": {
+1 -3
View File
@@ -7,9 +7,7 @@ module.exports = {
'abstract-name-filter-service',
'abstract-paperless-service',
],
transformIgnorePatterns: [
`<rootDir>/node_modules/.pnpm/(?!.*\\.mjs$|lodash-es)`,
],
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
moduleNameMapper: {
'^src/(.*)': '<rootDir>/src/$1',
},
+4 -11
View File
@@ -6185,39 +6185,32 @@
<context context-type="linenumber">43</context>
</context-group>
</trans-unit>
<trans-unit id="4524189418954028091" datatype="html">
<source>Hint: saved views can be created from the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a routerLink=&quot;/documents&quot;&gt;"/>documents list<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="6581372518205328477" datatype="html">
<source>Hello <x id="PH" equiv-text="this.settingsService.displayName"/>, welcome to <x id="PH_1" equiv-text="environment.appTitle"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">57</context>
</context-group>
</trans-unit>
<trans-unit id="2901300640157872718" datatype="html">
<source>Welcome to <x id="PH" equiv-text="environment.appTitle"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
<context context-type="linenumber">63</context>
<context context-type="linenumber">59</context>
</context-group>
</trans-unit>
<trans-unit id="1325877348738783391" datatype="html">
<source>Dashboard updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
<context context-type="linenumber">94</context>
<context context-type="linenumber">90</context>
</context-group>
</trans-unit>
<trans-unit id="3214475953924351473" datatype="html">
<source>Error updating dashboard</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
<context context-type="linenumber">97</context>
<context context-type="linenumber">93</context>
</context-group>
</trans-unit>
<trans-unit id="2946624699882754313" datatype="html">
+19419
View File
File diff suppressed because it is too large Load Diff
+24 -34
View File
@@ -2,7 +2,6 @@
"name": "paperless-ui",
"version": "0.0.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
"start": "ng serve",
"build": "ng build",
@@ -12,17 +11,17 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^19.2.2",
"@angular/common": "~19.2.1",
"@angular/compiler": "~19.2.1",
"@angular/core": "~19.2.1",
"@angular/forms": "~19.2.1",
"@angular/localize": "~19.2.1",
"@angular/platform-browser": "~19.2.1",
"@angular/platform-browser-dynamic": "~19.2.1",
"@angular/router": "~19.2.1",
"@angular/cdk": "^19.2.1",
"@angular/common": "~19.2.0",
"@angular/compiler": "~19.2.0",
"@angular/core": "~19.2.0",
"@angular/forms": "~19.2.0",
"@angular/localize": "~19.2.0",
"@angular/platform-browser": "~19.2.0",
"@angular/platform-browser-dynamic": "~19.2.0",
"@angular/router": "~19.2.0",
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
"@ng-select/ng-select": "^14.2.3",
"@ng-select/ng-select": "^14.2.2",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3",
@@ -44,24 +43,24 @@
"devDependencies": {
"@angular-builders/custom-webpack": "^19.0.0",
"@angular-builders/jest": "^19.0.0",
"@angular-devkit/build-angular": "^19.2.1",
"@angular-devkit/core": "^19.2.1",
"@angular-devkit/schematics": "^19.2.1",
"@angular-eslint/builder": "19.2.1",
"@angular-eslint/eslint-plugin": "19.2.1",
"@angular-eslint/eslint-plugin-template": "19.2.1",
"@angular-eslint/schematics": "19.2.1",
"@angular-eslint/template-parser": "19.2.1",
"@angular/cli": "~19.2.1",
"@angular/compiler-cli": "~19.2.1",
"@angular-devkit/build-angular": "^19.0.4",
"@angular-devkit/core": "^19.2.0",
"@angular-devkit/schematics": "^19.2.0",
"@angular-eslint/builder": "19.2.0",
"@angular-eslint/eslint-plugin": "19.2.0",
"@angular-eslint/eslint-plugin-template": "19.2.0",
"@angular-eslint/schematics": "19.2.0",
"@angular-eslint/template-parser": "19.2.0",
"@angular/cli": "~19.2.0",
"@angular/compiler-cli": "~19.2.0",
"@codecov/webpack-plugin": "^1.9.0",
"@playwright/test": "^1.50.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.9",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"@typescript-eslint/utils": "^8.26.1",
"eslint": "^9.22.0",
"@typescript-eslint/eslint-plugin": "^8.26.0",
"@typescript-eslint/parser": "^8.26.0",
"@typescript-eslint/utils": "^8.0.0",
"eslint": "^9.21.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0",
@@ -72,14 +71,5 @@
"ts-node": "~10.9.1",
"typescript": "^5.5.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"canvas",
"esbuild",
"lmdb",
"msgpackr-extract"
]
},
"typings": "./src/typings.d.ts"
}
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
port,
command: 'pnpm run start',
command: 'npm run start',
reuseExistingServer: !process.env.CI,
timeout: 2 * 60 * 1000,
},
-12447
View File
File diff suppressed because it is too large Load Diff
+1 -27
View File
@@ -36,13 +36,7 @@ export const routes: Routes = [
component: AppFrameComponent,
canDeactivate: [DirtyDocGuard],
children: [
{
path: 'dashboard',
component: DashboardComponent,
data: {
componentName: 'AppFrameComponent',
},
},
{ path: 'dashboard', component: DashboardComponent },
{
path: 'documents',
component: DocumentListComponent,
@@ -53,7 +47,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Document,
},
componentName: 'DocumentListComponent',
},
},
{
@@ -66,7 +59,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.SavedView,
},
componentName: 'DocumentListComponent',
},
},
{
@@ -78,7 +70,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Document,
},
componentName: 'DocumentDetailComponent',
},
},
{
@@ -90,7 +81,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Document,
},
componentName: 'DocumentDetailComponent',
},
},
{
@@ -102,7 +92,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Document,
},
componentName: 'DocumentAsnComponent',
},
},
{
@@ -114,7 +103,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Tag,
},
componentName: 'TagListComponent',
},
},
{
@@ -126,7 +114,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
componentName: 'DocumentTypeListComponent',
},
},
{
@@ -138,7 +125,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
componentName: 'CorrespondentListComponent',
},
},
{
@@ -150,7 +136,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.StoragePath,
},
componentName: 'StoragePathListComponent',
},
},
{
@@ -159,7 +144,6 @@ export const routes: Routes = [
canActivate: [PermissionsGuard],
data: {
requireAdmin: true,
componentName: 'LogsComponent',
},
},
{
@@ -171,7 +155,6 @@ export const routes: Routes = [
action: PermissionAction.Delete,
type: PermissionType.Document,
},
componentName: 'TrashComponent',
},
},
// redirect old paths
@@ -197,7 +180,6 @@ export const routes: Routes = [
action: PermissionAction.Change,
type: PermissionType.UISettings,
},
componentName: 'SettingsComponent',
},
},
{
@@ -210,7 +192,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.UISettings,
},
componentName: 'SettingsComponent',
},
},
{
@@ -222,7 +203,6 @@ export const routes: Routes = [
action: PermissionAction.Change,
type: PermissionType.AppConfig,
},
componentName: 'ConfigComponent',
},
},
{
@@ -234,7 +214,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.PaperlessTask,
},
componentName: 'TasksComponent',
},
},
{
@@ -246,7 +225,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.CustomField,
},
componentName: 'CustomFieldsComponent',
},
},
{
@@ -258,7 +236,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.Workflow,
},
componentName: 'WorkflowsComponent',
},
},
{
@@ -270,7 +247,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.MailAccount,
},
componentName: 'MailComponent',
},
},
{
@@ -282,7 +258,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.User,
},
componentName: 'UsersAndGroupsComponent',
},
},
{
@@ -294,7 +269,6 @@ export const routes: Routes = [
action: PermissionAction.View,
type: PermissionType.SavedView,
},
componentName: 'SavedViewsComponent',
},
},
],
@@ -34,17 +34,6 @@
}
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
@if (!settingsService.offerTour() && savedViewService.allViews.length === 0) {
<div class="col">
<div class="card shadow-sm bg-light opacity-50">
<div class="card-body">
<div class="text-center">
<p class="mb-0 fst-italic"><i-bs name="info-circle" class="me-2"></i-bs><ng-container i18n>Hint: saved views can be created from the <a routerLink="/documents">documents list</a></ng-container></p>
</div>
</div>
</div>
</div>
}
@for (v of dashboardViews; track v.id) {
<div class="col">
<pngx-saved-view-widget
@@ -105,7 +105,6 @@ describe('DashboardComponent', () => {
results: saved_views,
}),
dashboardViews: saved_views.filter((v) => v.show_on_dashboard),
allViews: saved_views,
},
},
provideHttpClient(withInterceptorsFromDi()),
@@ -6,8 +6,6 @@ import {
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { Component } from '@angular/core'
import { RouterModule } from '@angular/router'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -37,8 +35,6 @@ import { WelcomeWidgetComponent } from './widgets/welcome-widget/welcome-widget.
IfPermissionsDirective,
DragDropModule,
TourNgBootstrapModule,
NgxBootstrapIconsModule,
RouterModule,
],
})
export class DashboardComponent extends ComponentWithPermissions {
@@ -55,7 +55,7 @@
}
@case (DisplayField.TAGS) {
@for (tagID of doc.tags; track tagID) {
<pngx-tag [tagID]="tagID" class="ms-1 fs-6" (click)="clickTag(tagID, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
<pngx-tag [tagID]="tagID" class="ms-1" (click)="clickTag(tagID, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
}
}
@case (DisplayField.DOCUMENT_TYPE) {
@@ -310,8 +310,8 @@
</div>
}
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
@for (tagID of d.tags; track tagID) {
<pngx-tag [tagID]="tagID" class="ms-1 fs-6" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(tagID);$event.stopPropagation()"></pngx-tag>
@for (tagID of d.tags; track t) {
<pngx-tag [tagID]="tagID" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(tagID);$event.stopPropagation()"></pngx-tag>
}
}
</td>
@@ -872,7 +872,7 @@ export class FilterEditorComponent
let queries = this.customFieldQueriesModel.queries.map((query) =>
query.serialize()
)
if (queries.length > 0 && this.customFieldQueriesModel.isValid()) {
if (queries.length > 0) {
filterRules.push({
rule_type: FILTER_CUSTOM_FIELDS_QUERY,
value: JSON.stringify(queries[0]),
@@ -45,8 +45,7 @@ describe('CustomDatePipe', () => {
if (now.getMonth() === 0) {
notNow.setFullYear(now.getFullYear() - 1)
}
// weird options are for february...
expect(['Last month', '4 weeks ago', '3 weeks ago']).toContain(
expect(['Last month', '4 weeks ago']).toContain(
datePipe.transform(notNow, 'relative')
)
expect(datePipe.transform(now, 'relative')).toEqual('Just now')
@@ -29,7 +29,7 @@ describe('ComponentRouterService', () => {
eventsSubject.next(
new ActivationStart({
url: 'test-url',
data: { componentName: 'TestComponent' },
component: { name: 'TestComponent' },
} as any)
)
@@ -41,13 +41,13 @@ describe('ComponentRouterService', () => {
eventsSubject.next(
new ActivationStart({
url: 'test-url-1',
data: { componentName: 'TestComponent' },
component: { name: 'TestComponent' },
} as any)
)
eventsSubject.next(
new ActivationStart({
url: 'test-url-2',
data: { componentName: 'TestComponent' },
component: { name: 'TestComponent' },
} as any)
)
@@ -59,13 +59,13 @@ describe('ComponentRouterService', () => {
eventsSubject.next(
new ActivationStart({
url: 'test-url-1',
data: { componentName: 'TestComponent1' },
component: { name: 'TestComponent1' },
} as any)
)
eventsSubject.next(
new ActivationStart({
url: 'test-url-2',
data: { componentName: 'TestComponent2' },
component: { name: 'TestComponent2' },
} as any)
)
@@ -76,13 +76,13 @@ describe('ComponentRouterService', () => {
eventsSubject.next(
new ActivationStart({
url: 'test-url-1',
data: { componentName: 'TestComponent' },
component: { name: 'TestComponent' },
} as any)
)
eventsSubject.next(
new ActivationStart({
url: 'test-url-2',
data: { componentName: 'TestComponent' },
component: { name: 'TestComponent' },
} as any)
)
@@ -93,7 +93,7 @@ describe('ComponentRouterService', () => {
eventsSubject.next(
new ActivationStart({
url: 'test-url',
data: { componentName: 'TestComponent' },
component: { name: 'TestComponent' },
} as any)
)
@@ -17,11 +17,11 @@ export class ComponentRouterService {
.subscribe((event: ActivationStart) => {
if (
this.componentHistory[this.componentHistory.length - 1] !==
event.snapshot.data.componentName &&
!EXCLUDE_COMPONENTS.includes(event.snapshot.data.componentName)
event.snapshot.component.name &&
!EXCLUDE_COMPONENTS.includes(event.snapshot.component.name)
) {
this.history.push(event.snapshot.url.toString())
this.componentHistory.push(event.snapshot.data.componentName)
this.componentHistory.push(event.snapshot.component.name)
} else {
// Update the URL of the current component in case the same component was loaded via a different URL
this.history[this.history.length - 1] = event.snapshot.url.toString()
+2 -2
View File
@@ -3,9 +3,9 @@ const base_url = new URL(document.baseURI)
export const environment = {
production: true,
apiBaseUrl: document.baseURI + 'api/',
apiVersion: '7',
apiVersion: '8',
appTitle: 'Paperless-ngx',
version: '2.15.0',
version: '2.14.7',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',
+1 -1
View File
@@ -5,7 +5,7 @@
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:8000/api/',
apiVersion: '7',
apiVersion: '8',
appTitle: 'Paperless-ngx',
version: 'DEVELOPMENT',
webSocketHost: 'localhost:8000',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -294,9 +294,9 @@ class Command(BaseCommand):
inotify = INotify()
inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO | flags.MODIFY
if recursive:
inotify.add_watch_recursive(directory, inotify_flags)
descriptor = inotify.add_watch_recursive(directory, inotify_flags)
else:
inotify.add_watch(directory, inotify_flags)
descriptor = inotify.add_watch(directory, inotify_flags)
inotify_debounce_secs: Final[float] = settings.CONSUMER_INOTIFY_DELAY
inotify_debounce_ms: Final[int] = inotify_debounce_secs * 1000
@@ -305,55 +305,55 @@ class Command(BaseCommand):
notified_files = {}
try:
while not finished:
try:
for event in inotify.read(timeout=timeout_ms):
path = inotify.get_path(event.wd) if recursive else directory
filepath = os.path.join(path, event.name)
if flags.MODIFY in flags.from_mask(event.mask):
notified_files.pop(filepath, None)
else:
notified_files[filepath] = monotonic()
# Check the files against the timeout
still_waiting = {}
# last_event_time is time of the last inotify event for this file
for filepath, last_event_time in notified_files.items():
# Current time - last time over the configured timeout
waited_long_enough = (
monotonic() - last_event_time
) > inotify_debounce_secs
# Also make sure the file exists still, some scanners might write a
# temporary file first
file_still_exists = os.path.exists(filepath) and os.path.isfile(
filepath,
)
if waited_long_enough and file_still_exists:
_consume(filepath)
elif file_still_exists:
still_waiting[filepath] = last_event_time
# These files are still waiting to hit the timeout
notified_files = still_waiting
# If files are waiting, need to exit read() to check them
# Otherwise, go back to infinite sleep time, but only if not testing
if len(notified_files) > 0:
timeout_ms = inotify_debounce_ms
elif is_testing:
timeout_ms = self.testing_timeout_ms
while not finished:
try:
for event in inotify.read(timeout=timeout_ms):
path = inotify.get_path(event.wd) if recursive else directory
filepath = os.path.join(path, event.name)
if flags.MODIFY in flags.from_mask(event.mask):
notified_files.pop(filepath, None)
else:
timeout_ms = None
notified_files[filepath] = monotonic()
if self.stop_flag.is_set():
logger.debug("Finishing because event is set")
finished = True
# Check the files against the timeout
still_waiting = {}
# last_event_time is time of the last inotify event for this file
for filepath, last_event_time in notified_files.items():
# Current time - last time over the configured timeout
waited_long_enough = (
monotonic() - last_event_time
) > inotify_debounce_secs
except KeyboardInterrupt:
logger.info("Received SIGINT, stopping inotify")
# Also make sure the file exists still, some scanners might write a
# temporary file first
file_still_exists = os.path.exists(filepath) and os.path.isfile(
filepath,
)
if waited_long_enough and file_still_exists:
_consume(filepath)
elif file_still_exists:
still_waiting[filepath] = last_event_time
# These files are still waiting to hit the timeout
notified_files = still_waiting
# If files are waiting, need to exit read() to check them
# Otherwise, go back to infinite sleep time, but only if not testing
if len(notified_files) > 0:
timeout_ms = inotify_debounce_ms
elif is_testing:
timeout_ms = self.testing_timeout_ms
else:
timeout_ms = None
if self.stop_flag.is_set():
logger.debug("Finishing because event is set")
finished = True
finally:
inotify.close()
except KeyboardInterrupt:
logger.info("Received SIGINT, stopping inotify")
finished = True
inotify.rm_watch(descriptor)
inotify.close()
+49 -51
View File
@@ -70,59 +70,57 @@ def set_permissions_for_object(permissions: list[str], object, *, merge: bool =
for action in permissions:
permission = f"{action}_{object.__class__.__name__.lower()}"
if "users" in permissions[action]:
# users
users_to_add = User.objects.filter(id__in=permissions[action]["users"])
users_to_remove = (
get_users_with_perms(
object,
only_with_perms_in=[permission],
with_group_users=False,
)
if not merge
else User.objects.none()
# users
users_to_add = User.objects.filter(id__in=permissions[action]["users"])
users_to_remove = (
get_users_with_perms(
object,
only_with_perms_in=[permission],
with_group_users=False,
)
if len(users_to_add) > 0 and len(users_to_remove) > 0:
users_to_remove = users_to_remove.exclude(id__in=users_to_add)
if len(users_to_remove) > 0:
for user in users_to_remove:
remove_perm(permission, user, object)
if len(users_to_add) > 0:
for user in users_to_add:
assign_perm(permission, user, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
user,
object,
)
if "groups" in permissions[action]:
# groups
groups_to_add = Group.objects.filter(id__in=permissions[action]["groups"])
groups_to_remove = (
get_groups_with_only_permission(
object,
permission,
)
if not merge
else Group.objects.none()
if not merge
else User.objects.none()
)
if len(users_to_add) > 0 and len(users_to_remove) > 0:
users_to_remove = users_to_remove.exclude(id__in=users_to_add)
if len(users_to_remove) > 0:
for user in users_to_remove:
remove_perm(permission, user, object)
if len(users_to_add) > 0:
for user in users_to_add:
assign_perm(permission, user, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
user,
object,
)
# groups
groups_to_add = Group.objects.filter(id__in=permissions[action]["groups"])
groups_to_remove = (
get_groups_with_only_permission(
object,
permission,
)
if len(groups_to_add) > 0 and len(groups_to_remove) > 0:
groups_to_remove = groups_to_remove.exclude(id__in=groups_to_add)
if len(groups_to_remove) > 0:
for group in groups_to_remove:
remove_perm(permission, group, object)
if len(groups_to_add) > 0:
for group in groups_to_add:
assign_perm(permission, group, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
group,
object,
)
if not merge
else Group.objects.none()
)
if len(groups_to_add) > 0 and len(groups_to_remove) > 0:
groups_to_remove = groups_to_remove.exclude(id__in=groups_to_add)
if len(groups_to_remove) > 0:
for group in groups_to_remove:
remove_perm(permission, group, object)
if len(groups_to_add) > 0:
for group in groups_to_add:
assign_perm(permission, group, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
group,
object,
)
def get_objects_for_user_owner_aware(user, perms, Model) -> QuerySet:
+33 -43
View File
@@ -43,7 +43,6 @@ from documents.models import CustomFieldInstance
from documents.models import Document
from documents.models import DocumentType
from documents.models import MatchingModel
from documents.models import Note
from documents.models import PaperlessTask
from documents.models import SavedView
from documents.models import SavedViewFilterRule
@@ -160,24 +159,24 @@ class SetPermissionsMixin:
def validate_set_permissions(self, set_permissions=None):
permissions_dict = {
"view": {},
"change": {},
"view": {
"users": User.objects.none(),
"groups": Group.objects.none(),
},
"change": {
"users": User.objects.none(),
"groups": Group.objects.none(),
},
}
if set_permissions is not None:
for action in ["view", "change"]:
for action, _ in permissions_dict.items():
if action in set_permissions:
if "users" in set_permissions[action]:
users = set_permissions[action]["users"]
permissions_dict[action]["users"] = self._validate_user_ids(
users,
)
if "groups" in set_permissions[action]:
groups = set_permissions[action]["groups"]
permissions_dict[action]["groups"] = self._validate_group_ids(
groups,
)
else:
del permissions_dict[action]
users = set_permissions[action]["users"]
permissions_dict[action]["users"] = self._validate_user_ids(users)
groups = set_permissions[action]["groups"]
permissions_dict[action]["groups"] = self._validate_group_ids(
groups,
)
return permissions_dict
def _set_permissions(self, permissions, object):
@@ -422,6 +421,15 @@ class OwnedObjectListSerializer(serializers.ListSerializer):
return super().to_representation(documents)
class GetAPIVersionMixin:
def get_api_version(self):
return int(
self.context.get("request").version
if self.context.get("request")
else settings.REST_FRAMEWORK["DEFAULT_VERSION"],
)
class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
last_correspondence = serializers.DateTimeField(read_only=True, required=False)
@@ -718,7 +726,7 @@ class ReadWriteSerializerMethodField(serializers.SerializerMethodField):
return {self.field_name: data}
class CustomFieldInstanceSerializer(serializers.ModelSerializer):
class CustomFieldInstanceSerializer(serializers.ModelSerializer, GetAPIVersionMixin):
field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all())
value = ReadWriteSerializerMethodField(allow_null=True)
@@ -810,13 +818,6 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
return data
def get_api_version(self):
return int(
self.context.get("request").version
if self.context.get("request")
else settings.REST_FRAMEWORK["DEFAULT_VERSION"],
)
def to_internal_value(self, data):
ret = super().to_internal_value(data)
@@ -862,22 +863,6 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
]
class BasicUserSerializer(serializers.ModelSerializer):
# Different than paperless.serializers.UserSerializer
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name"]
class NotesSerializer(serializers.ModelSerializer):
user = BasicUserSerializer()
class Meta:
model = Note
fields = ["id", "note", "created", "user"]
ordering = ["-created"]
class DocumentSerializer(
OwnedObjectSerializer,
NestedUpdateMixin,
@@ -893,8 +878,6 @@ class DocumentSerializer(
created_date = serializers.DateField(required=False)
page_count = SerializerMethodField()
notes = NotesSerializer(many=True, required=False)
custom_fields = CustomFieldInstanceSerializer(
many=True,
allow_null=False,
@@ -1728,7 +1711,7 @@ class UiSettingsViewSerializer(serializers.ModelSerializer):
return ui_settings
class TasksViewSerializer(OwnedObjectSerializer):
class TasksViewSerializer(OwnedObjectSerializer, GetAPIVersionMixin):
class Meta:
model = PaperlessTask
fields = (
@@ -1771,6 +1754,13 @@ class TasksViewSerializer(OwnedObjectSerializer):
return result
def to_representation(self, instance: PaperlessTask):
result = super().to_representation(instance)
if self.get_api_version() < 8:
# Older versions only returned file tasks (filtering handled in view) and had different naming scheme
result["type"] = "file"
return result
class RunTaskViewSerializer(serializers.Serializer):
task_name = serializers.ChoiceField(
+1 -1
View File
@@ -1162,7 +1162,7 @@ def run_workflows(
) as f:
files = {
"file": (
filename,
document.original_filename,
f.read(),
document.mime_type,
),
@@ -6,7 +6,7 @@
{% endblock head_title %}
{% block form_top_content %}
<h4>{% translate "Account inactive." %}</h4>
<h4>{% translate "Account inactve." %}</h4>
{% endblock form_top_content %}
{% block form_content %}
-14
View File
@@ -2170,10 +2170,8 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
GIVEN:
- A document with a single note
WHEN:
- API request for document
- API request for document notes is made
THEN:
- Note is included in the document response
- The associated note is returned
"""
doc = Document.objects.create(
@@ -2187,18 +2185,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
user=self.user,
)
response = self.client.get(
f"/api/documents/{doc.pk}/",
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
resp_data = response.json()
self.assertEqual(len(resp_data["notes"]), 1)
self.assertEqual(resp_data["notes"][0]["note"], note.note)
self.assertEqual(resp_data["notes"][0]["user"]["username"], self.user.username)
response = self.client.get(
f"/api/documents/{doc.pk}/notes/",
format="json",
@@ -395,52 +395,6 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertTrue(checker.has_perm("view_document", doc))
self.assertIn("view_document", get_perms(group1, doc))
def test_patch_doesnt_remove_permissions(self):
"""
GIVEN:
- existing document with permissions set
WHEN:
- PATCH API request to update doc that is not json
THEN:
- Object permissions are not removed
"""
doc = Document.objects.create(
title="test",
mime_type="application/pdf",
content="this is a document",
)
user1 = User.objects.create_superuser(username="user1")
user2 = User.objects.create(username="user2")
group1 = Group.objects.create(name="group1")
doc.owner = user1
doc.save()
assign_perm("view_document", user2, doc)
assign_perm("change_document", user2, doc)
assign_perm("view_document", group1, doc)
assign_perm("change_document", group1, doc)
self.client.force_authenticate(user1)
response = self.client.patch(
f"/api/documents/{doc.id}/",
{
"archive_serial_number": "123",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
doc = Document.objects.get(pk=doc.id)
self.assertEqual(doc.owner, user1)
from guardian.core import ObjectPermissionChecker
checker = ObjectPermissionChecker(user2)
self.assertTrue(checker.has_perm("view_document", doc))
self.assertIn("view_document", get_perms(group1, doc))
self.assertTrue(checker.has_perm("change_document", doc))
self.assertIn("change_document", get_perms(group1, doc))
def test_dynamic_permissions_fields(self):
user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+42
View File
@@ -370,3 +370,45 @@ class TestTasks(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
mock_check_sanity.assert_not_called()
def test_handle_older_api_version(self):
"""
GIVEN:
- A request from the API with version < 8
WHEN:
- Tasks are requested
THEN:
- Only consume file tasks are returned and the type is 'file'
"""
task1 = PaperlessTask.objects.create(
task_id=str(uuid.uuid4()),
task_file_name="task_one.pdf",
task_name=PaperlessTask.TaskName.CONSUME_FILE,
)
task2 = PaperlessTask.objects.create(
task_id=str(uuid.uuid4()),
task_name=PaperlessTask.TaskName.CHECK_SANITY,
type=PaperlessTask.TaskType.AUTO,
)
response = self.client.get(
self.ENDPOINT,
headers={"Accept": "application/json; version=7"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["task_id"], task1.task_id)
self.assertEqual(response.data[0]["type"], "file")
response = self.client.get(
self.ENDPOINT,
headers={"Accept": "application/json; version=8"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)
self.assertEqual(response.data[0]["task_id"], task2.task_id)
self.assertEqual(response.data[0]["type"], PaperlessTask.TaskType.AUTO)
@@ -4,7 +4,6 @@ from pathlib import Path
from django.conf import settings
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import TestMigrations
@@ -15,7 +14,7 @@ def source_path_before(self):
return os.path.join(settings.ORIGINALS_DIR, fname)
class TestMigrateDocumentPageCount(DirectoriesMixin, TestMigrations):
class TestMigrateDocumentPageCount(TestMigrations):
migrate_from = "1052_document_transaction_id"
migrate_to = "1053_document_page_count"
+45 -10
View File
@@ -803,6 +803,33 @@ class DocumentViewSet(
except (FileNotFoundError, Document.DoesNotExist):
raise Http404
def getNotes(self, doc):
return [
{
"id": c.pk,
"note": c.note,
"created": c.created,
"user": {
"id": c.user.id,
"username": c.user.username,
"first_name": c.user.first_name,
"last_name": c.user.last_name,
},
}
for c in Note.objects.select_related("user")
.only(
"pk",
"note",
"created",
"user__id",
"user__username",
"user__first_name",
"user__last_name",
)
.filter(document=doc)
.order_by("-created")
]
@action(
methods=["get", "post", "delete"],
detail=True,
@@ -827,11 +854,9 @@ class DocumentViewSet(
except Document.DoesNotExist:
raise Http404
serializer = self.get_serializer(doc)
if request.method == "GET":
try:
notes = serializer.to_representation(doc).get("notes")
notes = self.getNotes(doc)
return Response(notes)
except Exception as e:
logger.warning(f"An error occurred retrieving notes: {e!s}")
@@ -872,7 +897,7 @@ class DocumentViewSet(
index.add_or_update_document(doc)
notes = serializer.to_representation(doc).get("notes")
notes = self.getNotes(doc)
return Response(notes)
except Exception as e:
@@ -909,9 +934,7 @@ class DocumentViewSet(
index.add_or_update_document(doc)
notes = serializer.to_representation(doc).get("notes")
return Response(notes)
return Response(self.getNotes(doc))
return Response(
{
@@ -2264,6 +2287,7 @@ class TasksViewSet(ReadOnlyModelViewSet):
ObjectOwnedOrGrantedPermissionsFilter,
)
filterset_class = PaperlessTaskFilterSet
queryset = PaperlessTask.objects.all().order_by("-date_created")
TASK_AND_ARGS_BY_NAME = {
PaperlessTask.TaskName.INDEX_OPTIMIZE: (index_optimize, {}),
@@ -2278,11 +2302,22 @@ class TasksViewSet(ReadOnlyModelViewSet):
}
def get_queryset(self):
queryset = PaperlessTask.objects.all().order_by("-date_created")
task_id = self.request.query_params.get("task_id")
if task_id is not None:
queryset = PaperlessTask.objects.filter(task_id=task_id)
return queryset
return PaperlessTask.objects.filter(task_id=task_id)
return super().get_queryset()
def list(self, request, *args, **kwargs):
version = int(
request.version if request else settings.REST_FRAMEWORK["DEFAULT_VERSION"],
)
if version < 8:
# Previous API versions only had file tasks, the format is also different (handled in serializer)
self.queryset = PaperlessTask.objects.filter(
task_name=PaperlessTask.TaskName.CONSUME_FILE,
).order_by("-date_created")
return super().list(request, *args, **kwargs)
@action(methods=["post"], detail=False)
def acknowledge(self, request):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-11 13:33-0700\n"
"POT-Creation-Date: 2025-03-06 14:46-0800\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1176,21 +1176,21 @@ msgstr ""
msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:135
#: documents/serialisers.py:134
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:561
#: documents/serialisers.py:560
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:1600
#: documents/serialisers.py:1581
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/serialisers.py:1689
#: documents/serialisers.py:1670
msgid "Invalid variable detected."
msgstr ""
@@ -1199,7 +1199,7 @@ msgid "Paperless-ngx account inactive"
msgstr ""
#: documents/templates/account/account_inactive.html:9
msgid "Account inactive."
msgid "Account inactve."
msgstr ""
#: documents/templates/account/account_inactive.html:14
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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