mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-28 22:59:03 -06:00
Compare commits
11 Commits
feature-be
...
feature-as
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efd65300a1 | ||
|
|
5af0d1da26 | ||
|
|
3281ec2401 | ||
|
|
dc9061eb97 | ||
|
|
6859e7e3c2 | ||
|
|
3e645bd9e2 | ||
|
|
09d39de200 | ||
|
|
94231dbb0f | ||
|
|
2f76350023 | ||
|
|
4cbe56e3af | ||
|
|
01b21377af |
19
.github/workflows/ci-docker.yml
vendored
19
.github/workflows/ci-docker.yml
vendored
@@ -46,14 +46,13 @@ jobs:
|
|||||||
id: ref
|
id: ref
|
||||||
run: |
|
run: |
|
||||||
ref_name="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
|
ref_name="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
|
||||||
# Sanitize by replacing / with - for cache keys
|
# Sanitize by replacing / with - for use in tags and cache keys
|
||||||
cache_ref="${ref_name//\//-}"
|
sanitized_ref="${ref_name//\//-}"
|
||||||
|
|
||||||
echo "ref_name=${ref_name}"
|
echo "ref_name=${ref_name}"
|
||||||
echo "cache_ref=${cache_ref}"
|
echo "sanitized_ref=${sanitized_ref}"
|
||||||
|
|
||||||
echo "name=${ref_name}" >> $GITHUB_OUTPUT
|
echo "name=${sanitized_ref}" >> $GITHUB_OUTPUT
|
||||||
echo "cache-ref=${cache_ref}" >> $GITHUB_OUTPUT
|
|
||||||
- name: Check push permissions
|
- name: Check push permissions
|
||||||
id: check-push
|
id: check-push
|
||||||
env:
|
env:
|
||||||
@@ -62,12 +61,14 @@ jobs:
|
|||||||
# should-push: Should we push to GHCR?
|
# should-push: Should we push to GHCR?
|
||||||
# True for:
|
# True for:
|
||||||
# 1. Pushes (tags/dev/beta) - filtered via the workflow triggers
|
# 1. Pushes (tags/dev/beta) - filtered via the workflow triggers
|
||||||
# 2. Internal PRs where the branch name starts with 'feature-' - filtered here when a PR is synced
|
# 2. Manual dispatch - always push to GHCR
|
||||||
|
# 3. Internal PRs where the branch name starts with 'feature-' or 'fix-'
|
||||||
|
|
||||||
should_push="false"
|
should_push="false"
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||||
should_push="true"
|
should_push="true"
|
||||||
|
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
|
should_push="true"
|
||||||
elif [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
|
elif [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
|
||||||
if [[ "${REF_NAME}" == feature-* || "${REF_NAME}" == fix-* ]]; then
|
if [[ "${REF_NAME}" == feature-* || "${REF_NAME}" == fix-* ]]; then
|
||||||
should_push="true"
|
should_push="true"
|
||||||
@@ -139,9 +140,9 @@ jobs:
|
|||||||
PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
|
PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
|
||||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.should-push }}
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.should-push }}
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.cache-ref }}-${{ matrix.arch }}
|
type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.name }}-${{ matrix.arch }}
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }}
|
type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }}
|
||||||
cache-to: ${{ steps.check-push.outputs.should-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }}
|
cache-to: ${{ steps.check-push.outputs.should-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.name, matrix.arch) || '' }}
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
if: steps.check-push.outputs.should-push == 'true'
|
if: steps.check-push.outputs.should-push == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -34,3 +34,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3143:3143" # IMAP
|
- "3143:3143" # IMAP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
nginx:
|
||||||
|
image: docker.io/nginx:1.29-alpine
|
||||||
|
hostname: nginx
|
||||||
|
container_name: nginx
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ../../docs/assets:/usr/share/nginx/html/assets:ro
|
||||||
|
- ./test-nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
|||||||
14
docker/compose/test-nginx.conf
Normal file
14
docker/compose/test-nginx.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
# Enable CORS for test requests
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS' always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -582,7 +582,7 @@ document.
|
|||||||
|
|
||||||
### Detecting duplicates {#fuzzy_duplicate}
|
### Detecting duplicates {#fuzzy_duplicate}
|
||||||
|
|
||||||
Paperless already catches and prevents upload of exactly matching documents,
|
Paperless-ngx already catches and warns of exactly matching documents,
|
||||||
however a new scan of an existing document may not produce an exact bit for bit
|
however a new scan of an existing document may not produce an exact bit for bit
|
||||||
duplicate. But the content should be exact or close, allowing detection.
|
duplicate. But the content should be exact or close, allowing detection.
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ classifiers = [
|
|||||||
# This will allow testing to not install a webserver, mysql, etc
|
# This will allow testing to not install a webserver, mysql, etc
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"adrf~=0.1.12",
|
||||||
"azure-ai-documentintelligence>=1.0.2",
|
"azure-ai-documentintelligence>=1.0.2",
|
||||||
"babel>=2.17",
|
"babel>=2.17",
|
||||||
"bleach~=6.3.0",
|
"bleach~=6.3.0",
|
||||||
@@ -300,6 +301,14 @@ norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ]
|
|||||||
|
|
||||||
DJANGO_SETTINGS_MODULE = "paperless.settings"
|
DJANGO_SETTINGS_MODULE = "paperless.settings"
|
||||||
|
|
||||||
|
markers = [
|
||||||
|
"live: Integration tests requiring external services (Gotenberg, Tika, nginx, etc)",
|
||||||
|
"nginx: Tests that make HTTP requests to the local nginx service",
|
||||||
|
"gotenberg: Tests requiring Gotenberg service",
|
||||||
|
"tika: Tests requiring Tika service",
|
||||||
|
"greenmail: Tests requiring Greenmail service",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.pytest_env]
|
[tool.pytest_env]
|
||||||
PAPERLESS_DISABLE_DBHANDLER = "true"
|
PAPERLESS_DISABLE_DBHANDLER = "true"
|
||||||
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
|
||||||
|
|||||||
@@ -561,7 +561,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">386</context>
|
<context context-type="linenumber">400</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>
|
||||||
@@ -1201,28 +1201,72 @@
|
|||||||
<source>Bulk editing</source>
|
<source>Bulk editing</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">263</context>
|
<context context-type="linenumber">264</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8158899674926420054" datatype="html">
|
<trans-unit id="8158899674926420054" datatype="html">
|
||||||
<source>Show confirmation dialogs</source>
|
<source>Show confirmation dialogs</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">266</context>
|
<context context-type="linenumber">267</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="290238406234356122" datatype="html">
|
<trans-unit id="290238406234356122" datatype="html">
|
||||||
<source>Apply on close</source>
|
<source>Apply on close</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">267</context>
|
<context context-type="linenumber">268</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5084275925647254161" datatype="html">
|
||||||
|
<source>PDF Editor</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">272</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
|
<context context-type="linenumber">66</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
|
<context context-type="linenumber">1472</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1577733187050997705" datatype="html">
|
||||||
|
<source>Default editing mode</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">275</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7273640930165035289" datatype="html">
|
||||||
|
<source>Create new document(s)</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">279</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">82</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8035757452478567832" datatype="html">
|
||||||
|
<source>Update existing document</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">280</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">87</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</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">271</context>
|
<context context-type="linenumber">285</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>
|
||||||
@@ -1241,14 +1285,14 @@
|
|||||||
<source>Enable notes</source>
|
<source>Enable 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">274</context>
|
<context context-type="linenumber">288</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</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">283</context>
|
<context context-type="linenumber">297</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>
|
||||||
@@ -1311,28 +1355,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">286</context>
|
<context context-type="linenumber">300</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6544153565064275581" datatype="html">
|
<trans-unit id="6544153565064275581" 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. but not documents) 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">290,292</context>
|
<context context-type="linenumber">304,306</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">297</context>
|
<context context-type="linenumber">311</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">301</context>
|
<context context-type="linenumber">315</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>
|
||||||
@@ -1343,18 +1387,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">306</context>
|
<context context-type="linenumber">320</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">311</context>
|
<context context-type="linenumber">325</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">338</context>
|
<context context-type="linenumber">352</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>
|
||||||
@@ -1385,11 +1429,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">321</context>
|
<context context-type="linenumber">335</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">348</context>
|
<context context-type="linenumber">362</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>
|
||||||
@@ -1420,14 +1464,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">333</context>
|
<context context-type="linenumber">347</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">357</context>
|
<context context-type="linenumber">371</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>
|
||||||
@@ -1446,7 +1490,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">365</context>
|
<context context-type="linenumber">379</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>
|
||||||
@@ -1457,49 +1501,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">368</context>
|
<context context-type="linenumber">382</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">372</context>
|
<context context-type="linenumber">386</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">373</context>
|
<context context-type="linenumber">387</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">374</context>
|
<context context-type="linenumber">388</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">375</context>
|
<context context-type="linenumber">389</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">375</context>
|
<context context-type="linenumber">389</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">385</context>
|
<context context-type="linenumber">399</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>
|
||||||
@@ -1570,21 +1614,21 @@
|
|||||||
<source>Use system language</source>
|
<source>Use system language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">78</context>
|
<context context-type="linenumber">79</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7729897675462249787" datatype="html">
|
<trans-unit id="7729897675462249787" datatype="html">
|
||||||
<source>Use date format of display language</source>
|
<source>Use date format of display language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">81</context>
|
<context context-type="linenumber">82</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1379170675585571971" datatype="html">
|
<trans-unit id="1379170675585571971" datatype="html">
|
||||||
<source>Archive serial number</source>
|
<source>Archive serial number</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">95</context>
|
<context context-type="linenumber">96</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
@@ -1595,7 +1639,7 @@
|
|||||||
<source>Correspondent</source>
|
<source>Correspondent</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">98</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
@@ -1626,7 +1670,7 @@
|
|||||||
<source>Document type</source>
|
<source>Document type</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">98</context>
|
<context context-type="linenumber">99</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
@@ -1657,7 +1701,7 @@
|
|||||||
<source>Storage path</source>
|
<source>Storage path</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">99</context>
|
<context context-type="linenumber">100</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
@@ -1684,7 +1728,7 @@
|
|||||||
<source>Tags</source>
|
<source>Tags</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">100</context>
|
<context context-type="linenumber">101</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
@@ -1723,7 +1767,7 @@
|
|||||||
<source>Error retrieving users</source>
|
<source>Error retrieving users</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">248</context>
|
<context context-type="linenumber">252</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
||||||
@@ -1734,7 +1778,7 @@
|
|||||||
<source>Error retrieving groups</source>
|
<source>Error retrieving groups</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">267</context>
|
<context context-type="linenumber">271</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
||||||
@@ -1745,28 +1789,28 @@
|
|||||||
<source>Settings were saved successfully.</source>
|
<source>Settings were saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">577</context>
|
<context context-type="linenumber">588</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="525012668859298131" datatype="html">
|
<trans-unit id="525012668859298131" datatype="html">
|
||||||
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
|
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">581</context>
|
<context context-type="linenumber">592</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8491974984518503778" datatype="html">
|
<trans-unit id="8491974984518503778" datatype="html">
|
||||||
<source>Reload now</source>
|
<source>Reload now</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">582</context>
|
<context context-type="linenumber">593</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3011185103048412841" datatype="html">
|
<trans-unit id="3011185103048412841" datatype="html">
|
||||||
<source>An error occurred while saving settings.</source>
|
<source>An error occurred while saving settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">592</context>
|
<context context-type="linenumber">603</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
@@ -2775,11 +2819,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1121</context>
|
<context context-type="linenumber">1108</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1486</context>
|
<context context-type="linenumber">1473</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -3370,7 +3414,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1074</context>
|
<context context-type="linenumber">1061</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -3475,7 +3519,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1537</context>
|
<context context-type="linenumber">1524</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6661109599266152398" datatype="html">
|
<trans-unit id="6661109599266152398" datatype="html">
|
||||||
@@ -3486,7 +3530,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1538</context>
|
<context context-type="linenumber">1525</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5162686434580248853" datatype="html">
|
<trans-unit id="5162686434580248853" datatype="html">
|
||||||
@@ -3497,7 +3541,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1539</context>
|
<context context-type="linenumber">1526</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8157388568390631653" datatype="html">
|
<trans-unit id="8157388568390631653" datatype="html">
|
||||||
@@ -6012,20 +6056,6 @@
|
|||||||
<context context-type="linenumber">70</context>
|
<context context-type="linenumber">70</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7273640930165035289" datatype="html">
|
|
||||||
<source>Create new document(s)</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
|
||||||
<context context-type="linenumber">82</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8035757452478567832" datatype="html">
|
|
||||||
<source>Update existing document</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
|
||||||
<context context-type="linenumber">87</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7248454234750442816" datatype="html">
|
<trans-unit id="7248454234750442816" datatype="html">
|
||||||
<source>Copy metadata</source>
|
<source>Copy metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -7373,17 +7403,6 @@
|
|||||||
<context context-type="linenumber">69</context>
|
<context context-type="linenumber">69</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5084275925647254161" datatype="html">
|
|
||||||
<source>PDF Editor</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">66</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1485</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2336375155355449543" datatype="html">
|
<trans-unit id="2336375155355449543" datatype="html">
|
||||||
<source>Remove Password</source>
|
<source>Remove Password</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -7619,56 +7638,56 @@
|
|||||||
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">441,443</context>
|
<context context-type="linenumber">428,430</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3200733026060976258" datatype="html">
|
<trans-unit id="3200733026060976258" datatype="html">
|
||||||
<source>Document changes detected</source>
|
<source>Document changes detected</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">480</context>
|
<context context-type="linenumber">467</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2887155916749964" datatype="html">
|
<trans-unit id="2887155916749964" datatype="html">
|
||||||
<source>The version of this document in your browser session appears older than the existing version.</source>
|
<source>The version of this document in your browser session appears older than the existing version.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">481</context>
|
<context context-type="linenumber">468</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="237142428785956348" datatype="html">
|
<trans-unit id="237142428785956348" datatype="html">
|
||||||
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">482</context>
|
<context context-type="linenumber">469</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8720977247725652816" datatype="html">
|
<trans-unit id="8720977247725652816" datatype="html">
|
||||||
<source>Ok</source>
|
<source>Ok</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">484</context>
|
<context context-type="linenumber">471</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6142395741265832184" datatype="html">
|
<trans-unit id="6142395741265832184" datatype="html">
|
||||||
<source>Next document</source>
|
<source>Next document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">610</context>
|
<context context-type="linenumber">597</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="651985345816518480" datatype="html">
|
<trans-unit id="651985345816518480" datatype="html">
|
||||||
<source>Previous document</source>
|
<source>Previous document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">620</context>
|
<context context-type="linenumber">607</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2885986061416655600" datatype="html">
|
<trans-unit id="2885986061416655600" datatype="html">
|
||||||
<source>Close document</source>
|
<source>Close document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">628</context>
|
<context context-type="linenumber">615</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
||||||
@@ -7679,67 +7698,67 @@
|
|||||||
<source>Save document</source>
|
<source>Save document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">635</context>
|
<context context-type="linenumber">622</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1784543155727940353" datatype="html">
|
<trans-unit id="1784543155727940353" datatype="html">
|
||||||
<source>Save and close / next</source>
|
<source>Save and close / next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">644</context>
|
<context context-type="linenumber">631</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5758784066858623886" datatype="html">
|
<trans-unit id="5758784066858623886" datatype="html">
|
||||||
<source>Error retrieving metadata</source>
|
<source>Error retrieving metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">699</context>
|
<context context-type="linenumber">686</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3456881259945295697" datatype="html">
|
<trans-unit id="3456881259945295697" datatype="html">
|
||||||
<source>Error retrieving suggestions.</source>
|
<source>Error retrieving suggestions.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">754</context>
|
<context context-type="linenumber">741</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2194092841814123758" datatype="html">
|
<trans-unit id="2194092841814123758" datatype="html">
|
||||||
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">963</context>
|
<context context-type="linenumber">950</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">987</context>
|
<context context-type="linenumber">974</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6626387786259219838" datatype="html">
|
<trans-unit id="6626387786259219838" datatype="html">
|
||||||
<source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source>
|
<source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">993</context>
|
<context context-type="linenumber">980</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="448882439049417053" datatype="html">
|
<trans-unit id="448882439049417053" datatype="html">
|
||||||
<source>Error saving document</source>
|
<source>Error saving document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1043</context>
|
<context context-type="linenumber">1030</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8410796510716511826" datatype="html">
|
<trans-unit id="8410796510716511826" datatype="html">
|
||||||
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
|
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1075</context>
|
<context context-type="linenumber">1062</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="282586936710748252" datatype="html">
|
<trans-unit id="282586936710748252" datatype="html">
|
||||||
<source>Documents can be restored prior to permanent deletion.</source>
|
<source>Documents can be restored prior to permanent deletion.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1076</context>
|
<context context-type="linenumber">1063</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -7750,7 +7769,7 @@
|
|||||||
<source>Move to trash</source>
|
<source>Move to trash</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1078</context>
|
<context context-type="linenumber">1065</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -7761,14 +7780,14 @@
|
|||||||
<source>Error deleting document</source>
|
<source>Error deleting document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1097</context>
|
<context context-type="linenumber">1084</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="619486176823357521" datatype="html">
|
<trans-unit id="619486176823357521" datatype="html">
|
||||||
<source>Reprocess confirm</source>
|
<source>Reprocess confirm</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1117</context>
|
<context context-type="linenumber">1104</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@@ -7779,102 +7798,102 @@
|
|||||||
<source>This operation will permanently recreate the archive file for this document.</source>
|
<source>This operation will permanently recreate the archive file for this document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1118</context>
|
<context context-type="linenumber">1105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="302054111564709516" datatype="html">
|
<trans-unit id="302054111564709516" datatype="html">
|
||||||
<source>The archive file will be re-generated with the current settings.</source>
|
<source>The archive file will be re-generated with the current settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1119</context>
|
<context context-type="linenumber">1106</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8251197608401006898" datatype="html">
|
<trans-unit id="8251197608401006898" datatype="html">
|
||||||
<source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
<source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1129</context>
|
<context context-type="linenumber">1116</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4409560272830824468" datatype="html">
|
<trans-unit id="4409560272830824468" datatype="html">
|
||||||
<source>Error executing operation</source>
|
<source>Error executing operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1140</context>
|
<context context-type="linenumber">1127</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6030453331794586802" datatype="html">
|
<trans-unit id="6030453331794586802" datatype="html">
|
||||||
<source>Error downloading document</source>
|
<source>Error downloading document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1189</context>
|
<context context-type="linenumber">1176</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4458954481601077369" datatype="html">
|
<trans-unit id="4458954481601077369" datatype="html">
|
||||||
<source>Page Fit</source>
|
<source>Page Fit</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1266</context>
|
<context context-type="linenumber">1253</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4663705961777238777" datatype="html">
|
<trans-unit id="4663705961777238777" datatype="html">
|
||||||
<source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
<source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1504</context>
|
<context context-type="linenumber">1491</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9043972994040261999" datatype="html">
|
<trans-unit id="9043972994040261999" datatype="html">
|
||||||
<source>Error executing PDF edit operation</source>
|
<source>Error executing PDF edit operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1516</context>
|
<context context-type="linenumber">1503</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6172690334763056188" datatype="html">
|
<trans-unit id="6172690334763056188" datatype="html">
|
||||||
<source>Please enter the current password before attempting to remove it.</source>
|
<source>Please enter the current password before attempting to remove it.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1527</context>
|
<context context-type="linenumber">1514</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="968660764814228922" datatype="html">
|
<trans-unit id="968660764814228922" datatype="html">
|
||||||
<source>Password removal operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
<source>Password removal operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1559</context>
|
<context context-type="linenumber">1546</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2282118435712883014" datatype="html">
|
<trans-unit id="2282118435712883014" datatype="html">
|
||||||
<source>Error executing password removal operation</source>
|
<source>Error executing password removal operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1573</context>
|
<context context-type="linenumber">1560</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3740891324955700797" datatype="html">
|
<trans-unit id="3740891324955700797" datatype="html">
|
||||||
<source>Print failed.</source>
|
<source>Print failed.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1610</context>
|
<context context-type="linenumber">1597</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6457245677384603573" datatype="html">
|
<trans-unit id="6457245677384603573" datatype="html">
|
||||||
<source>Error loading document for printing.</source>
|
<source>Error loading document for printing.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1622</context>
|
<context context-type="linenumber">1609</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6085793215710522488" datatype="html">
|
<trans-unit id="6085793215710522488" datatype="html">
|
||||||
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1687</context>
|
<context context-type="linenumber">1674</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1691</context>
|
<context context-type="linenumber">1678</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4958946940233632319" datatype="html">
|
<trans-unit id="4958946940233632319" datatype="html">
|
||||||
|
|||||||
@@ -259,6 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 ps-xl-5">
|
<div class="col-xl-6 ps-xl-5">
|
||||||
<h5 class="mt-3" i18n>Bulk editing</h5>
|
<h5 class="mt-3" i18n>Bulk editing</h5>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
@@ -268,6 +269,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-3" i18n>PDF Editor</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-form-label pt-0">
|
||||||
|
<span i18n>Default editing mode</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<select class="form-select" formControlName="pdfEditorDefaultEditMode">
|
||||||
|
<option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option>
|
||||||
|
<option [ngValue]="PdfEditorEditMode.Update" i18n>Update existing document</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="mt-3" i18n>Notes</h5>
|
<h5 class="mt-3" i18n>Notes</h5>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ describe('SettingsComponent', () => {
|
|||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toHaveBeenCalled()
|
||||||
expect(storeSpy).toHaveBeenCalled()
|
expect(storeSpy).toHaveBeenCalled()
|
||||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||||
expect(setSpy).toHaveBeenCalledTimes(31)
|
expect(setSpy).toHaveBeenCalledTimes(32)
|
||||||
|
|
||||||
// succeed
|
// succeed
|
||||||
storeSpy.mockReturnValueOnce(of(true))
|
storeSpy.mockReturnValueOnce(of(true))
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ import { PermissionsGroupComponent } from '../../common/input/permissions/permis
|
|||||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { PdfEditorEditMode } from '../../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
import { ZoomSetting } from '../../document-detail/document-detail.component'
|
import { ZoomSetting } from '../../document-detail/zoom-setting'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
@@ -163,6 +164,7 @@ export class SettingsComponent
|
|||||||
defaultPermsEditGroups: new FormControl(null),
|
defaultPermsEditGroups: new FormControl(null),
|
||||||
useNativePdfViewer: new FormControl(null),
|
useNativePdfViewer: new FormControl(null),
|
||||||
pdfViewerDefaultZoom: new FormControl(null),
|
pdfViewerDefaultZoom: new FormControl(null),
|
||||||
|
pdfEditorDefaultEditMode: new FormControl(null),
|
||||||
documentEditingRemoveInboxTags: new FormControl(null),
|
documentEditingRemoveInboxTags: new FormControl(null),
|
||||||
documentEditingOverlayThumbnail: new FormControl(null),
|
documentEditingOverlayThumbnail: new FormControl(null),
|
||||||
documentDetailsHiddenFields: new FormControl([]),
|
documentDetailsHiddenFields: new FormControl([]),
|
||||||
@@ -196,6 +198,8 @@ export class SettingsComponent
|
|||||||
|
|
||||||
public readonly ZoomSetting = ZoomSetting
|
public readonly ZoomSetting = ZoomSetting
|
||||||
|
|
||||||
|
public readonly PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
public readonly documentDetailFieldOptions = documentDetailFieldOptions
|
public readonly documentDetailFieldOptions = documentDetailFieldOptions
|
||||||
|
|
||||||
get systemStatusHasErrors(): boolean {
|
get systemStatusHasErrors(): boolean {
|
||||||
@@ -314,6 +318,9 @@ export class SettingsComponent
|
|||||||
pdfViewerDefaultZoom: this.settings.get(
|
pdfViewerDefaultZoom: this.settings.get(
|
||||||
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING
|
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING
|
||||||
),
|
),
|
||||||
|
pdfEditorDefaultEditMode: this.settings.get(
|
||||||
|
SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE
|
||||||
|
),
|
||||||
displayLanguage: this.settings.getLanguage(),
|
displayLanguage: this.settings.getLanguage(),
|
||||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||||
@@ -483,6 +490,10 @@ export class SettingsComponent
|
|||||||
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||||
this.settingsForm.value.pdfViewerDefaultZoom
|
this.settingsForm.value.pdfViewerDefaultZoom
|
||||||
)
|
)
|
||||||
|
this.settings.set(
|
||||||
|
SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE,
|
||||||
|
this.settingsForm.value.pdfEditorDefaultEditMode
|
||||||
|
)
|
||||||
this.settings.set(
|
this.settings.set(
|
||||||
SETTINGS_KEYS.DATE_LOCALE,
|
SETTINGS_KEYS.DATE_LOCALE,
|
||||||
this.settingsForm.value.dateLocale
|
this.settingsForm.value.dateLocale
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 366px) and (max-width: 768px) {
|
@media screen and (min-width: 376px) and (max-width: 768px) {
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
// compensate for 2 buttons on the right
|
// compensate for 2 buttons on the right
|
||||||
margin-right: 45px;
|
margin-right: 45px;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum PdfEditorEditMode {
|
||||||
|
Update = 'update',
|
||||||
|
Create = 'create',
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@ import { FormsModule } from '@angular/forms'
|
|||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
||||||
|
import { PdfEditorEditMode } from './pdf-editor-edit-mode'
|
||||||
|
|
||||||
interface PageOperation {
|
interface PageOperation {
|
||||||
page: number
|
page: number
|
||||||
@@ -19,11 +22,6 @@ interface PageOperation {
|
|||||||
loaded?: boolean
|
loaded?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PdfEditorEditMode {
|
|
||||||
Update = 'update',
|
|
||||||
Create = 'create',
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-pdf-editor',
|
selector: 'pngx-pdf-editor',
|
||||||
templateUrl: './pdf-editor.component.html',
|
templateUrl: './pdf-editor.component.html',
|
||||||
@@ -39,12 +37,15 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
|
|||||||
public PdfEditorEditMode = PdfEditorEditMode
|
public PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
private documentService = inject(DocumentService)
|
private documentService = inject(DocumentService)
|
||||||
|
private readonly settingsService = inject(SettingsService)
|
||||||
activeModal: NgbActiveModal = inject(NgbActiveModal)
|
activeModal: NgbActiveModal = inject(NgbActiveModal)
|
||||||
|
|
||||||
documentID: number
|
documentID: number
|
||||||
pages: PageOperation[] = []
|
pages: PageOperation[] = []
|
||||||
totalPages = 0
|
totalPages = 0
|
||||||
editMode: PdfEditorEditMode = PdfEditorEditMode.Create
|
editMode: PdfEditorEditMode = this.settingsService.get(
|
||||||
|
SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE
|
||||||
|
)
|
||||||
deleteOriginal: boolean = false
|
deleteOriginal: boolean = false
|
||||||
includeMetadata: boolean = true
|
includeMetadata: boolean = true
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,8 @@ import { environment } from 'src/environments/environment'
|
|||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||||
import {
|
import { DocumentDetailComponent } from './document-detail.component'
|
||||||
DocumentDetailComponent,
|
import { ZoomSetting } from './zoom-setting'
|
||||||
ZoomSetting,
|
|
||||||
} from './document-detail.component'
|
|
||||||
|
|
||||||
const doc: Document = {
|
const doc: Document = {
|
||||||
id: 3,
|
id: 3,
|
||||||
|
|||||||
@@ -106,16 +106,15 @@ import { TextComponent } from '../common/input/text/text.component'
|
|||||||
import { TextAreaComponent } from '../common/input/textarea/textarea.component'
|
import { TextAreaComponent } from '../common/input/textarea/textarea.component'
|
||||||
import { UrlComponent } from '../common/input/url/url.component'
|
import { UrlComponent } from '../common/input/url/url.component'
|
||||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||||
import {
|
import { PdfEditorEditMode } from '../common/pdf-editor/pdf-editor-edit-mode'
|
||||||
PDFEditorComponent,
|
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
||||||
PdfEditorEditMode,
|
|
||||||
} from '../common/pdf-editor/pdf-editor.component'
|
|
||||||
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
||||||
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
|
||||||
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
||||||
|
import { ZoomSetting } from './zoom-setting'
|
||||||
|
|
||||||
enum DocumentDetailNavIDs {
|
enum DocumentDetailNavIDs {
|
||||||
Details = 1,
|
Details = 1,
|
||||||
@@ -137,18 +136,6 @@ enum ContentRenderType {
|
|||||||
TIFF = 'tiff',
|
TIFF = 'tiff',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ZoomSetting {
|
|
||||||
PageFit = 'page-fit',
|
|
||||||
PageWidth = 'page-width',
|
|
||||||
Quarter = '.25',
|
|
||||||
Half = '.5',
|
|
||||||
ThreeQuarters = '.75',
|
|
||||||
One = '1',
|
|
||||||
OneAndHalf = '1.5',
|
|
||||||
Two = '2',
|
|
||||||
Three = '3',
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-document-detail',
|
selector: 'pngx-document-detail',
|
||||||
templateUrl: './document-detail.component.html',
|
templateUrl: './document-detail.component.html',
|
||||||
|
|||||||
11
src-ui/src/app/components/document-detail/zoom-setting.ts
Normal file
11
src-ui/src/app/components/document-detail/zoom-setting.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export enum ZoomSetting {
|
||||||
|
PageFit = 'page-fit',
|
||||||
|
PageWidth = 'page-width',
|
||||||
|
Quarter = '.25',
|
||||||
|
Half = '.5',
|
||||||
|
ThreeQuarters = '.75',
|
||||||
|
One = '1',
|
||||||
|
OneAndHalf = '1.5',
|
||||||
|
Two = '2',
|
||||||
|
Three = '3',
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
|
||||||
|
import { ZoomSetting } from '../components/document-detail/zoom-setting'
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
@@ -74,6 +76,8 @@ export const SETTINGS_KEYS = {
|
|||||||
'general-settings:document-details:hidden-fields',
|
'general-settings:document-details:hidden-fields',
|
||||||
SEARCH_DB_ONLY: 'general-settings:search:db-only',
|
SEARCH_DB_ONLY: 'general-settings:search:db-only',
|
||||||
SEARCH_FULL_TYPE: 'general-settings:search:more-link',
|
SEARCH_FULL_TYPE: 'general-settings:search:more-link',
|
||||||
|
PDF_EDITOR_DEFAULT_EDIT_MODE:
|
||||||
|
'general-settings:document-editing:default-edit-mode',
|
||||||
EMPTY_TRASH_DELAY: 'trash_delay',
|
EMPTY_TRASH_DELAY: 'trash_delay',
|
||||||
GMAIL_OAUTH_URL: 'gmail_oauth_url',
|
GMAIL_OAUTH_URL: 'gmail_oauth_url',
|
||||||
OUTLOOK_OAUTH_URL: 'outlook_oauth_url',
|
OUTLOOK_OAUTH_URL: 'outlook_oauth_url',
|
||||||
@@ -295,11 +299,16 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'page-width', // ZoomSetting from 'document-detail.component'
|
default: ZoomSetting.PageWidth,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.AI_ENABLED,
|
key: SETTINGS_KEYS.AI_ENABLED,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.PDF_EDITOR_DEFAULT_EDIT_MODE,
|
||||||
|
type: 'string',
|
||||||
|
default: PdfEditorEditMode.Create,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,30 +1,41 @@
|
|||||||
import { HttpEvent, HttpRequest } from '@angular/common/http'
|
import {
|
||||||
|
HttpClient,
|
||||||
|
provideHttpClient,
|
||||||
|
withInterceptors,
|
||||||
|
} from '@angular/common/http'
|
||||||
|
import {
|
||||||
|
HttpTestingController,
|
||||||
|
provideHttpClientTesting,
|
||||||
|
} from '@angular/common/http/testing'
|
||||||
import { TestBed } from '@angular/core/testing'
|
import { TestBed } from '@angular/core/testing'
|
||||||
import { of } from 'rxjs'
|
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { ApiVersionInterceptor } from './api-version.interceptor'
|
import { withApiVersionInterceptor } from './api-version.interceptor'
|
||||||
|
|
||||||
describe('ApiVersionInterceptor', () => {
|
describe('ApiVersionInterceptor', () => {
|
||||||
let interceptor: ApiVersionInterceptor
|
let httpClient: HttpClient
|
||||||
|
let httpMock: HttpTestingController
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [ApiVersionInterceptor],
|
providers: [
|
||||||
|
provideHttpClient(withInterceptors([withApiVersionInterceptor])),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
interceptor = TestBed.inject(ApiVersionInterceptor)
|
httpClient = TestBed.inject(HttpClient)
|
||||||
|
httpMock = TestBed.inject(HttpTestingController)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should add api version to headers', () => {
|
it('should add api version to headers', () => {
|
||||||
interceptor.intercept(new HttpRequest('GET', 'https://example.com'), {
|
httpClient.get('https://example.com').subscribe()
|
||||||
handle: (request) => {
|
const request = httpMock.expectOne('https://example.com')
|
||||||
const header = request.headers['lazyUpdate'][0]
|
const header = request.request.headers['lazyUpdate'][0]
|
||||||
expect(header.name).toEqual('Accept')
|
|
||||||
expect(header.value).toEqual(
|
expect(header.name).toEqual('Accept')
|
||||||
`application/json; version=${environment.apiVersion}`
|
expect(header.value).toEqual(
|
||||||
)
|
`application/json; version=${environment.apiVersion}`
|
||||||
return of({} as HttpEvent<any>)
|
)
|
||||||
},
|
request.flush({})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
HttpEvent,
|
HttpEvent,
|
||||||
HttpHandler,
|
HttpHandlerFn,
|
||||||
HttpInterceptor,
|
HttpInterceptorFn,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
} from '@angular/common/http'
|
} from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
export const withApiVersionInterceptor: HttpInterceptorFn = (
|
||||||
export class ApiVersionInterceptor implements HttpInterceptor {
|
request: HttpRequest<unknown>,
|
||||||
constructor() {}
|
next: HttpHandlerFn
|
||||||
|
): Observable<HttpEvent<unknown>> => {
|
||||||
intercept(
|
request = request.clone({
|
||||||
request: HttpRequest<unknown>,
|
setHeaders: {
|
||||||
next: HttpHandler
|
Accept: `application/json; version=${environment.apiVersion}`,
|
||||||
): Observable<HttpEvent<unknown>> {
|
},
|
||||||
request = request.clone({
|
})
|
||||||
setHeaders: {
|
return next(request)
|
||||||
Accept: `application/json; version=${environment.apiVersion}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return next.handle(request)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,52 @@
|
|||||||
import { HttpEvent, HttpRequest } from '@angular/common/http'
|
import {
|
||||||
|
HttpClient,
|
||||||
|
provideHttpClient,
|
||||||
|
withInterceptors,
|
||||||
|
} from '@angular/common/http'
|
||||||
|
import {
|
||||||
|
HttpTestingController,
|
||||||
|
provideHttpClientTesting,
|
||||||
|
} from '@angular/common/http/testing'
|
||||||
import { TestBed } from '@angular/core/testing'
|
import { TestBed } from '@angular/core/testing'
|
||||||
import { Meta } from '@angular/platform-browser'
|
import { Meta } from '@angular/platform-browser'
|
||||||
import { CookieService } from 'ngx-cookie-service'
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
import { of } from 'rxjs'
|
import { withCsrfInterceptor } from './csrf.interceptor'
|
||||||
import { CsrfInterceptor } from './csrf.interceptor'
|
|
||||||
|
|
||||||
describe('CsrfInterceptor', () => {
|
describe('CsrfInterceptor', () => {
|
||||||
let interceptor: CsrfInterceptor
|
|
||||||
let meta: Meta
|
let meta: Meta
|
||||||
let cookieService: CookieService
|
let cookieService: CookieService
|
||||||
|
let httpClient: HttpClient
|
||||||
|
let httpMock: HttpTestingController
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [CsrfInterceptor, Meta, CookieService],
|
providers: [
|
||||||
|
Meta,
|
||||||
|
CookieService,
|
||||||
|
provideHttpClient(withInterceptors([withCsrfInterceptor])),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
meta = TestBed.inject(Meta)
|
meta = TestBed.inject(Meta)
|
||||||
cookieService = TestBed.inject(CookieService)
|
cookieService = TestBed.inject(CookieService)
|
||||||
interceptor = TestBed.inject(CsrfInterceptor)
|
httpClient = TestBed.inject(HttpClient)
|
||||||
|
httpMock = TestBed.inject(HttpTestingController)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get csrf token', () => {
|
it('should get csrf token', () => {
|
||||||
meta.addTag({ name: 'cookie_prefix', content: 'ngx-' }, true)
|
meta.addTag({ name: 'cookie_prefix', content: 'ngx-' }, true)
|
||||||
|
|
||||||
const cookieServiceSpy = jest.spyOn(cookieService, 'get')
|
const cookieServiceSpy = jest.spyOn(cookieService, 'get')
|
||||||
cookieServiceSpy.mockReturnValue('csrftoken')
|
cookieServiceSpy.mockReturnValue('csrftoken')
|
||||||
interceptor.intercept(new HttpRequest('GET', 'https://example.com'), {
|
|
||||||
handle: (request) => {
|
httpClient.get('https://example.com').subscribe()
|
||||||
expect(request.headers['lazyUpdate'][0]['name']).toEqual('X-CSRFToken')
|
const request = httpMock.expectOne('https://example.com')
|
||||||
return of({} as HttpEvent<any>)
|
|
||||||
},
|
expect(request.request.headers['lazyUpdate'][0]['name']).toEqual(
|
||||||
})
|
'X-CSRFToken'
|
||||||
|
)
|
||||||
expect(cookieServiceSpy).toHaveBeenCalled()
|
expect(cookieServiceSpy).toHaveBeenCalled()
|
||||||
|
request.flush({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,36 +1,32 @@
|
|||||||
import {
|
import {
|
||||||
HttpEvent,
|
HttpEvent,
|
||||||
HttpHandler,
|
HttpHandlerFn,
|
||||||
HttpInterceptor,
|
HttpInterceptorFn,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
} from '@angular/common/http'
|
} from '@angular/common/http'
|
||||||
import { inject, Injectable } from '@angular/core'
|
import { inject } from '@angular/core'
|
||||||
import { Meta } from '@angular/platform-browser'
|
import { Meta } from '@angular/platform-browser'
|
||||||
import { CookieService } from 'ngx-cookie-service'
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
@Injectable()
|
export const withCsrfInterceptor: HttpInterceptorFn = (
|
||||||
export class CsrfInterceptor implements HttpInterceptor {
|
request: HttpRequest<unknown>,
|
||||||
private cookieService: CookieService = inject(CookieService)
|
next: HttpHandlerFn
|
||||||
private meta: Meta = inject(Meta)
|
): Observable<HttpEvent<unknown>> => {
|
||||||
|
const cookieService: CookieService = inject(CookieService)
|
||||||
|
const meta: Meta = inject(Meta)
|
||||||
|
|
||||||
intercept(
|
let prefix = ''
|
||||||
request: HttpRequest<unknown>,
|
if (meta.getTag('name=cookie_prefix')) {
|
||||||
next: HttpHandler
|
prefix = meta.getTag('name=cookie_prefix').content
|
||||||
): Observable<HttpEvent<unknown>> {
|
|
||||||
let prefix = ''
|
|
||||||
if (this.meta.getTag('name=cookie_prefix')) {
|
|
||||||
prefix = this.meta.getTag('name=cookie_prefix').content
|
|
||||||
}
|
|
||||||
let csrfToken = this.cookieService.get(`${prefix}csrftoken`)
|
|
||||||
if (csrfToken) {
|
|
||||||
request = request.clone({
|
|
||||||
setHeaders: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return next.handle(request)
|
|
||||||
}
|
}
|
||||||
|
let csrfToken = cookieService.get(`${prefix}csrftoken`)
|
||||||
|
if (csrfToken) {
|
||||||
|
request = request.clone({
|
||||||
|
setHeaders: {
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return next(request)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { DatePipe, registerLocaleData } from '@angular/common'
|
import { DatePipe, registerLocaleData } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
HTTP_INTERCEPTORS,
|
|
||||||
provideHttpClient,
|
provideHttpClient,
|
||||||
withFetch,
|
withFetch,
|
||||||
|
withInterceptors,
|
||||||
withInterceptorsFromDi,
|
withInterceptorsFromDi,
|
||||||
} from '@angular/common/http'
|
} from '@angular/common/http'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
@@ -151,8 +151,8 @@ import { AppComponent } from './app/app.component'
|
|||||||
import { DirtyDocGuard } from './app/guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './app/guards/dirty-doc.guard'
|
||||||
import { DirtySavedViewGuard } from './app/guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './app/guards/dirty-saved-view.guard'
|
||||||
import { PermissionsGuard } from './app/guards/permissions.guard'
|
import { PermissionsGuard } from './app/guards/permissions.guard'
|
||||||
import { ApiVersionInterceptor } from './app/interceptors/api-version.interceptor'
|
import { withApiVersionInterceptor } from './app/interceptors/api-version.interceptor'
|
||||||
import { CsrfInterceptor } from './app/interceptors/csrf.interceptor'
|
import { withCsrfInterceptor } from './app/interceptors/csrf.interceptor'
|
||||||
import { DocumentTitlePipe } from './app/pipes/document-title.pipe'
|
import { DocumentTitlePipe } from './app/pipes/document-title.pipe'
|
||||||
import { FilterPipe } from './app/pipes/filter.pipe'
|
import { FilterPipe } from './app/pipes/filter.pipe'
|
||||||
import { UsernamePipe } from './app/pipes/username.pipe'
|
import { UsernamePipe } from './app/pipes/username.pipe'
|
||||||
@@ -381,16 +381,6 @@ bootstrapApplication(AppComponent, {
|
|||||||
provideAppInitializer(initializeApp),
|
provideAppInitializer(initializeApp),
|
||||||
DatePipe,
|
DatePipe,
|
||||||
CookieService,
|
CookieService,
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: CsrfInterceptor,
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: ApiVersionInterceptor,
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||||
@@ -402,6 +392,10 @@ bootstrapApplication(AppComponent, {
|
|||||||
CorrespondentNamePipe,
|
CorrespondentNamePipe,
|
||||||
DocumentTypeNamePipe,
|
DocumentTypeNamePipe,
|
||||||
StoragePathNamePipe,
|
StoragePathNamePipe,
|
||||||
provideHttpClient(withInterceptorsFromDi(), withFetch()),
|
provideHttpClient(
|
||||||
|
withInterceptorsFromDi(),
|
||||||
|
withInterceptors([withCsrfInterceptor, withApiVersionInterceptor]),
|
||||||
|
withFetch()
|
||||||
|
),
|
||||||
],
|
],
|
||||||
}).catch((err) => console.error(err))
|
}).catch((err) => console.error(err))
|
||||||
|
|||||||
@@ -501,9 +501,22 @@ class Command(BaseCommand):
|
|||||||
stability_timeout_ms = int(stability_delay * 1000)
|
stability_timeout_ms = int(stability_delay * 1000)
|
||||||
testing_timeout_ms = int(self.testing_timeout_s * 1000)
|
testing_timeout_ms = int(self.testing_timeout_s * 1000)
|
||||||
|
|
||||||
# Start with no timeout (wait indefinitely for first event)
|
# Calculate appropriate timeout for watch loop
|
||||||
# unless in testing mode
|
# In polling mode, rust_timeout must be significantly longer than poll_delay_ms
|
||||||
timeout_ms = testing_timeout_ms if is_testing else 0
|
# to ensure poll cycles can complete before timing out
|
||||||
|
if is_testing:
|
||||||
|
if use_polling:
|
||||||
|
# For polling: timeout must be at least 3x the poll interval to allow
|
||||||
|
# multiple poll cycles. This prevents timeouts from interfering with
|
||||||
|
# the polling mechanism.
|
||||||
|
min_polling_timeout_ms = poll_delay_ms * 3
|
||||||
|
timeout_ms = max(min_polling_timeout_ms, testing_timeout_ms)
|
||||||
|
else:
|
||||||
|
# For native watching, use short timeout to check stop flag
|
||||||
|
timeout_ms = testing_timeout_ms
|
||||||
|
else:
|
||||||
|
# Not testing, wait indefinitely for first event
|
||||||
|
timeout_ms = 0
|
||||||
|
|
||||||
self.stop_flag.clear()
|
self.stop_flag.clear()
|
||||||
|
|
||||||
@@ -543,8 +556,14 @@ class Command(BaseCommand):
|
|||||||
# Check pending files at stability interval
|
# Check pending files at stability interval
|
||||||
timeout_ms = stability_timeout_ms
|
timeout_ms = stability_timeout_ms
|
||||||
elif is_testing:
|
elif is_testing:
|
||||||
# In testing, use short timeout to check stop flag
|
# In testing, use appropriate timeout based on watch mode
|
||||||
timeout_ms = testing_timeout_ms
|
if use_polling:
|
||||||
|
# For polling: ensure timeout allows polls to complete
|
||||||
|
min_polling_timeout_ms = poll_delay_ms * 3
|
||||||
|
timeout_ms = max(min_polling_timeout_ms, testing_timeout_ms)
|
||||||
|
else:
|
||||||
|
# For native watching, use short timeout to check stop flag
|
||||||
|
timeout_ms = testing_timeout_ms
|
||||||
else: # pragma: nocover
|
else: # pragma: nocover
|
||||||
# No pending files, wait indefinitely
|
# No pending files, wait indefinitely
|
||||||
timeout_ms = 0
|
timeout_ms = 0
|
||||||
|
|||||||
@@ -114,6 +114,30 @@ def mock_supported_extensions(mocker: MockerFixture) -> MagicMock:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_mock_call(
|
||||||
|
mock_obj: MagicMock,
|
||||||
|
timeout_s: float = 5.0,
|
||||||
|
poll_interval_s: float = 0.1,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Actively wait for a mock to be called.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mock_obj: The mock object to check (e.g., mock.delay)
|
||||||
|
timeout_s: Maximum time to wait in seconds
|
||||||
|
poll_interval_s: How often to check in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if mock was called within timeout, False otherwise
|
||||||
|
"""
|
||||||
|
start_time = monotonic()
|
||||||
|
while monotonic() - start_time < timeout_s:
|
||||||
|
if mock_obj.called:
|
||||||
|
return True
|
||||||
|
sleep(poll_interval_s)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TestTrackedFile:
|
class TestTrackedFile:
|
||||||
"""Tests for the TrackedFile dataclass."""
|
"""Tests for the TrackedFile dataclass."""
|
||||||
|
|
||||||
@@ -724,7 +748,7 @@ def start_consumer(
|
|||||||
thread = ConsumerThread(consumption_dir, scratch_dir, **kwargs)
|
thread = ConsumerThread(consumption_dir, scratch_dir, **kwargs)
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
thread.start()
|
thread.start()
|
||||||
sleep(0.5) # Give thread time to start
|
sleep(2.0) # Give thread time to start
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -767,7 +791,8 @@ class TestCommandWatch:
|
|||||||
|
|
||||||
target = consumption_dir / "document.pdf"
|
target = consumption_dir / "document.pdf"
|
||||||
shutil.copy(sample_pdf, target)
|
shutil.copy(sample_pdf, target)
|
||||||
sleep(0.5)
|
|
||||||
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
@@ -788,9 +813,12 @@ class TestCommandWatch:
|
|||||||
|
|
||||||
thread = start_consumer()
|
thread = start_consumer()
|
||||||
|
|
||||||
|
sleep(0.5)
|
||||||
|
|
||||||
target = consumption_dir / "document.pdf"
|
target = consumption_dir / "document.pdf"
|
||||||
shutil.move(temp_location, target)
|
shutil.move(temp_location, target)
|
||||||
sleep(0.5)
|
|
||||||
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
@@ -816,7 +844,7 @@ class TestCommandWatch:
|
|||||||
f.flush()
|
f.flush()
|
||||||
sleep(0.05)
|
sleep(0.05)
|
||||||
|
|
||||||
sleep(0.5)
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
@@ -837,7 +865,7 @@ class TestCommandWatch:
|
|||||||
(consumption_dir / "._document.pdf").write_bytes(b"test")
|
(consumption_dir / "._document.pdf").write_bytes(b"test")
|
||||||
shutil.copy(sample_pdf, consumption_dir / "valid.pdf")
|
shutil.copy(sample_pdf, consumption_dir / "valid.pdf")
|
||||||
|
|
||||||
sleep(0.5)
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
@@ -868,11 +896,10 @@ class TestCommandWatch:
|
|||||||
assert not thread.is_alive()
|
assert not thread.is_alive()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
class TestCommandWatchPolling:
|
class TestCommandWatchPolling:
|
||||||
"""Tests for polling mode."""
|
"""Tests for polling mode."""
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.flaky(reruns=2)
|
|
||||||
def test_polling_mode_works(
|
def test_polling_mode_works(
|
||||||
self,
|
self,
|
||||||
consumption_dir: Path,
|
consumption_dir: Path,
|
||||||
@@ -882,7 +909,8 @@ class TestCommandWatchPolling:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Test polling mode detects files.
|
Test polling mode detects files.
|
||||||
Note: At times, there appears to be a timing issue, where delay has not yet been called, hence this is marked as flaky.
|
|
||||||
|
Uses active waiting with timeout to handle CI delays and polling timing.
|
||||||
"""
|
"""
|
||||||
# Use shorter polling interval for faster test
|
# Use shorter polling interval for faster test
|
||||||
thread = start_consumer(polling_interval=0.5, stability_delay=0.1)
|
thread = start_consumer(polling_interval=0.5, stability_delay=0.1)
|
||||||
@@ -890,9 +918,9 @@ class TestCommandWatchPolling:
|
|||||||
target = consumption_dir / "document.pdf"
|
target = consumption_dir / "document.pdf"
|
||||||
shutil.copy(sample_pdf, target)
|
shutil.copy(sample_pdf, target)
|
||||||
|
|
||||||
# Wait for: poll interval + stability delay + another poll + margin
|
# Actively wait for consumption
|
||||||
# CI can be slow, so use generous timeout
|
# Polling needs: interval (0.5s) + stability (0.1s) + next poll (0.5s) + margin
|
||||||
sleep(3.0)
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=5.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
@@ -919,7 +947,8 @@ class TestCommandWatchRecursive:
|
|||||||
|
|
||||||
target = subdir / "document.pdf"
|
target = subdir / "document.pdf"
|
||||||
shutil.copy(sample_pdf, target)
|
shutil.copy(sample_pdf, target)
|
||||||
sleep(0.5)
|
|
||||||
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
@@ -948,7 +977,8 @@ class TestCommandWatchRecursive:
|
|||||||
|
|
||||||
target = subdir / "document.pdf"
|
target = subdir / "document.pdf"
|
||||||
shutil.copy(sample_pdf, target)
|
shutil.copy(sample_pdf, target)
|
||||||
sleep(0.5)
|
|
||||||
|
wait_for_mock_call(mock_consume_file_delay.delay, timeout_s=2.0)
|
||||||
|
|
||||||
if thread.exception:
|
if thread.exception:
|
||||||
raise thread.exception
|
raise thread.exception
|
||||||
|
|||||||
@@ -89,3 +89,11 @@ def greenmail_mail_account(db: None) -> Generator[MailAccount, None, None]:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def mail_account_handler() -> MailAccountHandler:
|
def mail_account_handler() -> MailAccountHandler:
|
||||||
return MailAccountHandler()
|
return MailAccountHandler()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def nginx_base_url() -> Generator[str, None, None]:
|
||||||
|
"""
|
||||||
|
The base URL for the nginx HTTP server we expect to be alive
|
||||||
|
"""
|
||||||
|
yield "http://localhost:8080"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Content-Transfer-Encoding: 7bit
|
|||||||
<p>Some Text</p>
|
<p>Some Text</p>
|
||||||
<p>
|
<p>
|
||||||
<img src="cid:part1.pNdUSz0s.D3NqVtPg@example.de" alt="Has to be rewritten to work..">
|
<img src="cid:part1.pNdUSz0s.D3NqVtPg@example.de" alt="Has to be rewritten to work..">
|
||||||
<img src="https://docs.paperless-ngx.com/assets/logo_full_white.svg" alt="This image should not be shown.">
|
<img src="http://localhost:8080/assets/logo_full_white.svg" alt="This image should not be shown.">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>and an embedded image.<br>
|
<p>and an embedded image.<br>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<p>Some Text</p>
|
<p>Some Text</p>
|
||||||
<p>
|
<p>
|
||||||
<img src="cid:part1.pNdUSz0s.D3NqVtPg@example.de" alt="Has to be rewritten to work..">
|
<img src="cid:part1.pNdUSz0s.D3NqVtPg@example.de" alt="Has to be rewritten to work..">
|
||||||
<img src="https://docs.paperless-ngx.com/assets/logo_full_white.svg" alt="This image should not be shown.">
|
<img src="http://localhost:8080/assets/logo_full_white.svg" alt="This image should not be shown.">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>and an embedded image.<br>
|
<p>and an embedded image.<br>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from paperless_mail.models import MailAccount
|
|||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.live
|
||||||
|
@pytest.mark.greenmail
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestMailGreenmail:
|
class TestMailGreenmail:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from paperless_mail.parsers import MailDocumentParser
|
|||||||
def extract_text(pdf_path: Path) -> str:
|
def extract_text(pdf_path: Path) -> str:
|
||||||
"""
|
"""
|
||||||
Using pdftotext from poppler, extracts the text of a PDF into a file,
|
Using pdftotext from poppler, extracts the text of a PDF into a file,
|
||||||
then reads the file contents and returns it
|
then reads the file contents and returns it.
|
||||||
"""
|
"""
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
mode="w+",
|
mode="w+",
|
||||||
@@ -38,71 +38,107 @@ def extract_text(pdf_path: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class MailAttachmentMock:
|
class MailAttachmentMock:
|
||||||
def __init__(self, payload, content_id):
|
def __init__(self, payload: bytes, content_id: str) -> None:
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.content_id = content_id
|
self.content_id = content_id
|
||||||
self.content_type = "image/png"
|
self.content_type = "image/png"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.live
|
||||||
|
@pytest.mark.nginx
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"PAPERLESS_CI_TEST" not in os.environ,
|
"PAPERLESS_CI_TEST" not in os.environ,
|
||||||
reason="No Gotenberg/Tika servers to test with",
|
reason="No Gotenberg/Tika servers to test with",
|
||||||
)
|
)
|
||||||
class TestUrlCanary:
|
class TestNginxService:
|
||||||
"""
|
"""
|
||||||
Verify certain URLs are still available so testing is valid still
|
Verify the local nginx server is responding correctly.
|
||||||
|
These tests validate that the test infrastructure is working properly
|
||||||
|
before running the actual parser tests that depend on HTTP resources.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_online_image_exception_on_not_available(self):
|
def test_non_existent_resource_returns_404(
|
||||||
|
self,
|
||||||
|
nginx_base_url: str,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Fresh start
|
- Local nginx server is running
|
||||||
WHEN:
|
WHEN:
|
||||||
- nonexistent image is requested
|
- A non-existent resource is requested
|
||||||
THEN:
|
THEN:
|
||||||
- An exception shall be thrown
|
- An HTTP 404 status code shall be returned
|
||||||
"""
|
|
||||||
"""
|
|
||||||
A public image is used in the html sample file. We have no control
|
|
||||||
whether this image stays online forever, so here we check if we can detect if is not
|
|
||||||
available anymore.
|
|
||||||
"""
|
"""
|
||||||
resp = httpx.get(
|
resp = httpx.get(
|
||||||
"https://docs.paperless-ngx.com/assets/non-existent.png",
|
f"{nginx_base_url}/assets/non-existent.png",
|
||||||
|
timeout=5.0,
|
||||||
)
|
)
|
||||||
with pytest.raises(httpx.HTTPStatusError) as exec_info:
|
with pytest.raises(httpx.HTTPStatusError) as exec_info:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
assert exec_info.value.response.status_code == httpx.codes.NOT_FOUND
|
assert exec_info.value.response.status_code == httpx.codes.NOT_FOUND
|
||||||
|
|
||||||
def test_is_online_image_still_available(self):
|
def test_valid_resource_is_available(
|
||||||
|
self,
|
||||||
|
nginx_base_url: str,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Fresh start
|
- Local nginx server is running
|
||||||
WHEN:
|
WHEN:
|
||||||
- A public image used in the html sample file is requested
|
- A valid test fixture resource is requested
|
||||||
THEN:
|
THEN:
|
||||||
- No exception shall be thrown
|
- The resource shall be returned with HTTP 200 status code
|
||||||
|
- The response shall contain the expected content type
|
||||||
"""
|
"""
|
||||||
"""
|
|
||||||
A public image is used in the html sample file. We have no control
|
|
||||||
whether this image stays online forever, so here we check if it is still there
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Now check the URL used in samples/sample.html
|
|
||||||
resp = httpx.get(
|
resp = httpx.get(
|
||||||
"https://docs.paperless-ngx.com/assets/logo_full_white.svg",
|
f"{nginx_base_url}/assets/logo_full_white.svg",
|
||||||
|
timeout=5.0,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
assert resp.status_code == httpx.codes.OK
|
||||||
|
assert "svg" in resp.headers.get("content-type", "").lower()
|
||||||
|
|
||||||
|
def test_server_connectivity(
|
||||||
|
self,
|
||||||
|
nginx_base_url: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Local test fixtures server should be running
|
||||||
|
WHEN:
|
||||||
|
- A request is made to the server root
|
||||||
|
THEN:
|
||||||
|
- The server shall respond without connection errors
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = httpx.get(
|
||||||
|
nginx_base_url,
|
||||||
|
timeout=5.0,
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
# We don't care about the status code, just that we can connect
|
||||||
|
assert resp.status_code in {200, 404, 403}
|
||||||
|
except httpx.ConnectError as e:
|
||||||
|
pytest.fail(
|
||||||
|
f"Cannot connect to nginx server at {nginx_base_url}. "
|
||||||
|
f"Ensure the nginx container is running via docker-compose.ci-test.yml. "
|
||||||
|
f"Error: {e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.live
|
||||||
|
@pytest.mark.gotenberg
|
||||||
|
@pytest.mark.tika
|
||||||
|
@pytest.mark.nginx
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"PAPERLESS_CI_TEST" not in os.environ,
|
"PAPERLESS_CI_TEST" not in os.environ,
|
||||||
reason="No Gotenberg/Tika servers to test with",
|
reason="No Gotenberg/Tika servers to test with",
|
||||||
)
|
)
|
||||||
class TestParserLive:
|
class TestParserLive:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def imagehash(file, hash_size=18):
|
def imagehash(file: Path, hash_size: int = 18) -> str:
|
||||||
return f"{average_hash(Image.open(file), hash_size)}"
|
return f"{average_hash(Image.open(file), hash_size)}"
|
||||||
|
|
||||||
def test_get_thumbnail(
|
def test_get_thumbnail(
|
||||||
@@ -112,14 +148,15 @@ class TestParserLive:
|
|||||||
simple_txt_email_file: Path,
|
simple_txt_email_file: Path,
|
||||||
simple_txt_email_pdf_file: Path,
|
simple_txt_email_pdf_file: Path,
|
||||||
simple_txt_email_thumbnail_file: Path,
|
simple_txt_email_thumbnail_file: Path,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Fresh start
|
- A simple text email file
|
||||||
|
- Mocked PDF generation returning a known PDF
|
||||||
WHEN:
|
WHEN:
|
||||||
- The Thumbnail is requested
|
- The thumbnail is requested
|
||||||
THEN:
|
THEN:
|
||||||
- The returned thumbnail image file is as expected
|
- The returned thumbnail image file shall match the expected hash
|
||||||
"""
|
"""
|
||||||
mock_generate_pdf = mocker.patch(
|
mock_generate_pdf = mocker.patch(
|
||||||
"paperless_mail.parsers.MailDocumentParser.generate_pdf",
|
"paperless_mail.parsers.MailDocumentParser.generate_pdf",
|
||||||
@@ -134,22 +171,28 @@ class TestParserLive:
|
|||||||
assert self.imagehash(thumb) == self.imagehash(
|
assert self.imagehash(thumb) == self.imagehash(
|
||||||
simple_txt_email_thumbnail_file,
|
simple_txt_email_thumbnail_file,
|
||||||
), (
|
), (
|
||||||
f"Created Thumbnail {thumb} differs from expected file {simple_txt_email_thumbnail_file}"
|
f"Created thumbnail {thumb} differs from expected file "
|
||||||
|
f"{simple_txt_email_thumbnail_file}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tika_parse_successful(self, mail_parser: MailDocumentParser):
|
def test_tika_parse_successful(self, mail_parser: MailDocumentParser) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Fresh start
|
- HTML content to parse
|
||||||
|
- Tika server is running
|
||||||
WHEN:
|
WHEN:
|
||||||
- tika parsing is called
|
- Tika parsing is called
|
||||||
THEN:
|
THEN:
|
||||||
- a web request to tika shall be done and the reply es returned
|
- A web request to Tika shall be made
|
||||||
|
- The parsed text content shall be returned
|
||||||
"""
|
"""
|
||||||
html = '<html><head><meta http-equiv="content-type" content="text/html; charset=UTF-8"></head><body><p>Some Text</p></body></html>'
|
html = (
|
||||||
|
'<html><head><meta http-equiv="content-type" '
|
||||||
|
'content="text/html; charset=UTF-8"></head>'
|
||||||
|
"<body><p>Some Text</p></body></html>"
|
||||||
|
)
|
||||||
expected_text = "Some Text"
|
expected_text = "Some Text"
|
||||||
|
|
||||||
# Check successful parsing
|
|
||||||
parsed = mail_parser.tika_parse(html)
|
parsed = mail_parser.tika_parse(html)
|
||||||
assert expected_text == parsed.strip()
|
assert expected_text == parsed.strip()
|
||||||
|
|
||||||
@@ -160,14 +203,17 @@ class TestParserLive:
|
|||||||
html_email_file: Path,
|
html_email_file: Path,
|
||||||
merged_pdf_first: Path,
|
merged_pdf_first: Path,
|
||||||
merged_pdf_second: Path,
|
merged_pdf_second: Path,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Intermediary pdfs to be merged
|
- Intermediary PDFs to be merged
|
||||||
|
- An HTML email file
|
||||||
WHEN:
|
WHEN:
|
||||||
- pdf generation is requested with html file requiring merging of pdfs
|
- PDF generation is requested with HTML file requiring merging
|
||||||
THEN:
|
THEN:
|
||||||
- gotenberg is called to merge files and the resulting file is returned
|
- Gotenberg shall be called to merge files
|
||||||
|
- The resulting merged PDF shall be returned
|
||||||
|
- The merged PDF shall contain text from both source PDFs
|
||||||
"""
|
"""
|
||||||
mock_generate_pdf_from_html = mocker.patch(
|
mock_generate_pdf_from_html = mocker.patch(
|
||||||
"paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html",
|
"paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html",
|
||||||
@@ -200,16 +246,17 @@ class TestParserLive:
|
|||||||
html_email_file: Path,
|
html_email_file: Path,
|
||||||
html_email_pdf_file: Path,
|
html_email_pdf_file: Path,
|
||||||
html_email_thumbnail_file: Path,
|
html_email_thumbnail_file: Path,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Fresh start
|
- An HTML email file
|
||||||
WHEN:
|
WHEN:
|
||||||
- pdf generation from simple eml file is requested
|
- PDF generation from the email file is requested
|
||||||
THEN:
|
THEN:
|
||||||
- Gotenberg is called and the resulting file is returned and look as expected.
|
- Gotenberg shall be called to generate the PDF
|
||||||
|
- The archive PDF shall contain the expected content
|
||||||
|
- The generated thumbnail shall match the expected image hash
|
||||||
"""
|
"""
|
||||||
|
|
||||||
util_call_with_backoff(mail_parser.parse, [html_email_file, "message/rfc822"])
|
util_call_with_backoff(mail_parser.parse, [html_email_file, "message/rfc822"])
|
||||||
|
|
||||||
# Check the archive PDF
|
# Check the archive PDF
|
||||||
@@ -217,7 +264,7 @@ class TestParserLive:
|
|||||||
archive_text = extract_text(archive_path)
|
archive_text = extract_text(archive_path)
|
||||||
expected_archive_text = extract_text(html_email_pdf_file)
|
expected_archive_text = extract_text(html_email_pdf_file)
|
||||||
|
|
||||||
# Archive includes the HTML content, so use in
|
# Archive includes the HTML content
|
||||||
assert expected_archive_text in archive_text
|
assert expected_archive_text in archive_text
|
||||||
|
|
||||||
# Check the thumbnail
|
# Check the thumbnail
|
||||||
@@ -227,9 +274,12 @@ class TestParserLive:
|
|||||||
)
|
)
|
||||||
generated_thumbnail_hash = self.imagehash(generated_thumbnail)
|
generated_thumbnail_hash = self.imagehash(generated_thumbnail)
|
||||||
|
|
||||||
# The created pdf is not reproducible. But the converted image should always look the same.
|
# The created PDF is not reproducible, but the converted image
|
||||||
|
# should always look the same
|
||||||
expected_hash = self.imagehash(html_email_thumbnail_file)
|
expected_hash = self.imagehash(html_email_thumbnail_file)
|
||||||
|
|
||||||
assert generated_thumbnail_hash == expected_hash, (
|
assert generated_thumbnail_hash == expected_hash, (
|
||||||
f"PDF looks different. Check if {generated_thumbnail} looks weird."
|
f"PDF thumbnail differs from expected. "
|
||||||
|
f"Generated: {generated_thumbnail}, "
|
||||||
|
f"Hash: {generated_thumbnail_hash} vs {expected_hash}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from adrf.views import APIView
|
||||||
|
from adrf.viewsets import ModelViewSet
|
||||||
|
from adrf.viewsets import ReadOnlyModelViewSet
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
@@ -15,11 +20,9 @@ from httpx_oauth.oauth2 import GetAccessTokenError
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework.generics import GenericAPIView
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
|
||||||
|
|
||||||
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
||||||
from documents.permissions import PaperlessObjectPermissions
|
from documents.permissions import PaperlessObjectPermissions
|
||||||
@@ -39,6 +42,8 @@ from paperless_mail.serialisers import MailRuleSerializer
|
|||||||
from paperless_mail.serialisers import ProcessedMailSerializer
|
from paperless_mail.serialisers import ProcessedMailSerializer
|
||||||
from paperless_mail.tasks import process_mail_accounts
|
from paperless_mail.tasks import process_mail_accounts
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("paperless_mail")
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
test=extend_schema(
|
test=extend_schema(
|
||||||
@@ -66,71 +71,75 @@ from paperless_mail.tasks import process_mail_accounts
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
class MailAccountViewSet(ModelViewSet, PassUserMixin):
|
class MailAccountViewSet(ModelViewSet, PassUserMixin):
|
||||||
model = MailAccount
|
|
||||||
|
|
||||||
queryset = MailAccount.objects.all().order_by("pk")
|
queryset = MailAccount.objects.all().order_by("pk")
|
||||||
serializer_class = MailAccountSerializer
|
serializer_class = MailAccountSerializer
|
||||||
pagination_class = StandardPagination
|
pagination_class = StandardPagination
|
||||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,)
|
filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,)
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self) -> list[Any]:
|
||||||
if self.action == "test":
|
if self.action == "test":
|
||||||
# Test action does not require object level permissions
|
return [IsAuthenticated()]
|
||||||
self.permission_classes = (IsAuthenticated,)
|
|
||||||
return super().get_permissions()
|
return super().get_permissions()
|
||||||
|
|
||||||
@action(methods=["post"], detail=False)
|
@action(methods=["post"], detail=False)
|
||||||
def test(self, request):
|
async def test(self, request: Request) -> Response | HttpResponseBadRequest:
|
||||||
logger = logging.getLogger("paperless_mail")
|
|
||||||
request.data["name"] = datetime.datetime.now().isoformat()
|
request.data["name"] = datetime.datetime.now().isoformat()
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
# account exists, use the password from there instead of *** and refresh_token / expiration
|
# Validation must be wrapped because of sync DB validators
|
||||||
|
await sync_to_async(serializer.is_valid)(raise_exception=True)
|
||||||
|
|
||||||
|
validated_data: dict[str, Any] = serializer.validated_data
|
||||||
|
|
||||||
if (
|
if (
|
||||||
len(serializer.validated_data.get("password").replace("*", "")) == 0
|
len(str(validated_data.get("password", "")).replace("*", "")) == 0
|
||||||
and request.data["id"] is not None
|
and request.data.get("id") is not None
|
||||||
):
|
):
|
||||||
existing_account = MailAccount.objects.get(pk=request.data["id"])
|
existing_account = await MailAccount.objects.aget(pk=request.data["id"])
|
||||||
serializer.validated_data["password"] = existing_account.password
|
validated_data.update(
|
||||||
serializer.validated_data["account_type"] = existing_account.account_type
|
{
|
||||||
serializer.validated_data["refresh_token"] = existing_account.refresh_token
|
"password": existing_account.password,
|
||||||
serializer.validated_data["expiration"] = existing_account.expiration
|
"account_type": existing_account.account_type,
|
||||||
|
"refresh_token": existing_account.refresh_token,
|
||||||
|
"expiration": existing_account.expiration,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
account = MailAccount(**serializer.validated_data)
|
account = MailAccount(**validated_data)
|
||||||
with get_mailbox(
|
|
||||||
account.imap_server,
|
def _blocking_imap_test() -> bool:
|
||||||
account.imap_port,
|
with get_mailbox(
|
||||||
account.imap_security,
|
account.imap_server,
|
||||||
) as M:
|
account.imap_port,
|
||||||
try:
|
account.imap_security,
|
||||||
|
) as m_box:
|
||||||
if (
|
if (
|
||||||
account.is_token
|
account.is_token
|
||||||
and account.expiration is not None
|
and account.expiration
|
||||||
and account.expiration < timezone.now()
|
and account.expiration < timezone.now()
|
||||||
):
|
):
|
||||||
oauth_manager = PaperlessMailOAuth2Manager()
|
oauth_manager = PaperlessMailOAuth2Manager()
|
||||||
if oauth_manager.refresh_account_oauth_token(existing_account):
|
if oauth_manager.refresh_account_oauth_token(existing_account):
|
||||||
# User is not changing password and token needs to be refreshed
|
# User is not changing password and token needs to be refreshed
|
||||||
existing_account.refresh_from_db()
|
|
||||||
account.password = existing_account.password
|
account.password = existing_account.password
|
||||||
else:
|
else:
|
||||||
raise MailError("Unable to refresh oauth token")
|
raise MailError("Unable to refresh oauth token")
|
||||||
|
mailbox_login(m_box, account)
|
||||||
|
return True
|
||||||
|
|
||||||
mailbox_login(M, account)
|
try:
|
||||||
return Response({"success": True})
|
await sync_to_async(_blocking_imap_test, thread_sensitive=False)()
|
||||||
except MailError as e:
|
return Response({"success": True})
|
||||||
logger.error(
|
except MailError as e:
|
||||||
f"Mail account {account} test failed: {e}",
|
logger.error(f"Mail account {account} test failed: {e}")
|
||||||
)
|
return HttpResponseBadRequest("Unable to connect to server")
|
||||||
return HttpResponseBadRequest("Unable to connect to server")
|
|
||||||
|
|
||||||
@action(methods=["post"], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
def process(self, request, pk=None):
|
async def process(self, request: Request, pk: int | None = None) -> Response:
|
||||||
account = self.get_object()
|
# FIX: Use aget_object() provided by adrf to avoid SynchronousOnlyOperation
|
||||||
|
account = await self.aget_object()
|
||||||
process_mail_accounts.delay([account.pk])
|
process_mail_accounts.delay([account.pk])
|
||||||
|
|
||||||
return Response({"result": "OK"})
|
return Response({"result": "OK"})
|
||||||
|
|
||||||
|
|
||||||
@@ -144,21 +153,38 @@ class ProcessedMailViewSet(ReadOnlyModelViewSet, PassUserMixin):
|
|||||||
ObjectOwnedOrGrantedPermissionsFilter,
|
ObjectOwnedOrGrantedPermissionsFilter,
|
||||||
)
|
)
|
||||||
filterset_class = ProcessedMailFilterSet
|
filterset_class = ProcessedMailFilterSet
|
||||||
|
|
||||||
queryset = ProcessedMail.objects.all().order_by("-processed")
|
queryset = ProcessedMail.objects.all().order_by("-processed")
|
||||||
|
|
||||||
@action(methods=["post"], detail=False)
|
@action(methods=["post"], detail=False)
|
||||||
def bulk_delete(self, request):
|
async def bulk_delete(
|
||||||
mail_ids = request.data.get("mail_ids", [])
|
self,
|
||||||
|
request: Request,
|
||||||
|
) -> Response | HttpResponseBadRequest | HttpResponseForbidden:
|
||||||
|
mail_ids: list[int] = request.data.get("mail_ids", [])
|
||||||
if not isinstance(mail_ids, list) or not all(
|
if not isinstance(mail_ids, list) or not all(
|
||||||
isinstance(i, int) for i in mail_ids
|
isinstance(i, int) for i in mail_ids
|
||||||
):
|
):
|
||||||
return HttpResponseBadRequest("mail_ids must be a list of integers")
|
return HttpResponseBadRequest("mail_ids must be a list of integers")
|
||||||
mails = ProcessedMail.objects.filter(id__in=mail_ids)
|
|
||||||
for mail in mails:
|
# Store objects to delete after verification
|
||||||
if not has_perms_owner_aware(request.user, "delete_processedmail", mail):
|
to_delete: list[ProcessedMail] = []
|
||||||
|
|
||||||
|
# We must verify permissions for every requested ID
|
||||||
|
async for mail in ProcessedMail.objects.filter(id__in=mail_ids):
|
||||||
|
can_delete = await sync_to_async(has_perms_owner_aware)(
|
||||||
|
request.user,
|
||||||
|
"delete_processedmail",
|
||||||
|
mail,
|
||||||
|
)
|
||||||
|
if not can_delete:
|
||||||
|
# This is what the test is looking for: 403 on permission failure
|
||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
mail.delete()
|
to_delete.append(mail)
|
||||||
|
|
||||||
|
# Only perform deletions if all items passed the permission check
|
||||||
|
for mail in to_delete:
|
||||||
|
await mail.adelete()
|
||||||
|
|
||||||
return Response({"result": "OK", "deleted_mail_ids": mail_ids})
|
return Response({"result": "OK", "deleted_mail_ids": mail_ids})
|
||||||
|
|
||||||
|
|
||||||
@@ -178,77 +204,74 @@ class MailRuleViewSet(ModelViewSet, PassUserMixin):
|
|||||||
responses={200: None},
|
responses={200: None},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
class OauthCallbackView(GenericAPIView):
|
class OauthCallbackView(APIView):
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
def get(self, request, format=None):
|
async def get(
|
||||||
if not (
|
self,
|
||||||
request.user and request.user.has_perms(["paperless_mail.add_mailaccount"])
|
request: Request,
|
||||||
):
|
) -> Response | HttpResponseBadRequest | HttpResponseRedirect:
|
||||||
|
has_perm = await sync_to_async(request.user.has_perm)(
|
||||||
|
"paperless_mail.add_mailaccount",
|
||||||
|
)
|
||||||
|
if not has_perm:
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
"You do not have permission to add mail accounts",
|
"You do not have permission to add mail accounts",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("paperless_mail")
|
code: str | None = request.query_params.get("code")
|
||||||
code = request.query_params.get("code")
|
state: str | None = request.query_params.get("state")
|
||||||
# Gmail passes scope as a query param, Outlook does not
|
scope: str | None = request.query_params.get("scope")
|
||||||
scope = request.query_params.get("scope")
|
|
||||||
|
|
||||||
if code is None:
|
if not code or not state:
|
||||||
logger.error(
|
return HttpResponseBadRequest("Invalid request parameters")
|
||||||
f"Invalid oauth callback request, code: {code}, scope: {scope}",
|
|
||||||
)
|
|
||||||
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
|
||||||
|
|
||||||
oauth_manager = PaperlessMailOAuth2Manager(
|
oauth_manager = PaperlessMailOAuth2Manager(
|
||||||
state=request.session.get("oauth_state"),
|
state=request.session.get("oauth_state"),
|
||||||
)
|
)
|
||||||
|
|
||||||
state = request.query_params.get("state", "")
|
|
||||||
if not oauth_manager.validate_state(state):
|
if not oauth_manager.validate_state(state):
|
||||||
logger.error(
|
return HttpResponseBadRequest("Invalid OAuth state")
|
||||||
f"Invalid oauth callback request received state: {state}, expected: {oauth_manager.state}",
|
|
||||||
)
|
|
||||||
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if scope is not None and "google" in scope:
|
defaults: dict[str, Any] = {
|
||||||
# Google
|
"username": "",
|
||||||
|
"imap_security": MailAccount.ImapSecurity.SSL,
|
||||||
|
"imap_port": 993,
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope and "google" in scope:
|
||||||
account_type = MailAccount.MailAccountType.GMAIL_OAUTH
|
account_type = MailAccount.MailAccountType.GMAIL_OAUTH
|
||||||
imap_server = "imap.gmail.com"
|
imap_server = "imap.gmail.com"
|
||||||
defaults = {
|
defaults.update(
|
||||||
"name": f"Gmail OAuth {timezone.now()}",
|
{
|
||||||
"username": "",
|
"name": f"Gmail OAuth {timezone.now()}",
|
||||||
"imap_security": MailAccount.ImapSecurity.SSL,
|
"account_type": account_type,
|
||||||
"imap_port": 993,
|
},
|
||||||
"account_type": account_type,
|
)
|
||||||
}
|
result = await sync_to_async(oauth_manager.get_gmail_access_token)(code)
|
||||||
result = oauth_manager.get_gmail_access_token(code)
|
else:
|
||||||
|
|
||||||
elif scope is None:
|
|
||||||
# Outlook
|
|
||||||
account_type = MailAccount.MailAccountType.OUTLOOK_OAUTH
|
account_type = MailAccount.MailAccountType.OUTLOOK_OAUTH
|
||||||
imap_server = "outlook.office365.com"
|
imap_server = "outlook.office365.com"
|
||||||
defaults = {
|
defaults.update(
|
||||||
"name": f"Outlook OAuth {timezone.now()}",
|
{
|
||||||
"username": "",
|
"name": f"Outlook OAuth {timezone.now()}",
|
||||||
"imap_security": MailAccount.ImapSecurity.SSL,
|
"account_type": account_type,
|
||||||
"imap_port": 993,
|
},
|
||||||
"account_type": account_type,
|
)
|
||||||
}
|
result = await sync_to_async(oauth_manager.get_outlook_access_token)(
|
||||||
|
code,
|
||||||
|
)
|
||||||
|
|
||||||
result = oauth_manager.get_outlook_access_token(code)
|
account, _ = await MailAccount.objects.aupdate_or_create(
|
||||||
|
|
||||||
access_token = result["access_token"]
|
|
||||||
refresh_token = result["refresh_token"]
|
|
||||||
expires_in = result["expires_in"]
|
|
||||||
account, _ = MailAccount.objects.update_or_create(
|
|
||||||
password=access_token,
|
|
||||||
is_token=True,
|
|
||||||
imap_server=imap_server,
|
imap_server=imap_server,
|
||||||
refresh_token=refresh_token,
|
refresh_token=result["refresh_token"],
|
||||||
expiration=timezone.now() + timedelta(seconds=expires_in),
|
defaults={
|
||||||
defaults=defaults,
|
**defaults,
|
||||||
|
"password": result["access_token"],
|
||||||
|
"is_token": True,
|
||||||
|
"expiration": timezone.now()
|
||||||
|
+ timedelta(seconds=result["expires_in"]),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
f"{oauth_manager.oauth_redirect_url}?oauth_success=1&account_id={account.pk}",
|
f"{oauth_manager.oauth_redirect_url}?oauth_success=1&account_id={account.pk}",
|
||||||
|
|||||||
25
uv.lock
generated
25
uv.lock
generated
@@ -16,6 +16,20 @@ supported-markers = [
|
|||||||
"sys_platform == 'linux'",
|
"sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adrf"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "async-property", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "djangorestframework", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/10/fe/573d7ec8805aec8e13459451f81398c6ac819882496e82fb6b4ae96c2762/adrf-0.1.12.tar.gz", hash = "sha256:e7aa49e5406b168f040f1a12cafb606e98fdd5467314240a9c42dbe63200d2c1", size = 17181, upload-time = "2025-11-24T03:25:44.337Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/47/14006c4045f818cf625d2357e823a15b4bb7fab8dd81a40acd23af41ee53/adrf-0.1.12-py3-none-any.whl", hash = "sha256:e9d1f343b82158f4c528c0809c9635a27ceef4c37d3d8e61b8096c8eeded616d", size = 20199, upload-time = "2025-11-24T03:25:43.291Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohappyeyeballs"
|
name = "aiohappyeyeballs"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -203,6 +217,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-property"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/12/900eb34b3af75c11b69d6b78b74ec0fd1ba489376eceb3785f787d1a0a1d/async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380", size = 16523, upload-time = "2023-07-03T17:21:55.688Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7", size = 9546, upload-time = "2023-07-03T17:21:54.293Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-timeout"
|
name = "async-timeout"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
@@ -2919,6 +2942,7 @@ name = "paperless-ngx"
|
|||||||
version = "2.20.5"
|
version = "2.20.5"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "adrf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -3067,6 +3091,7 @@ typing = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "adrf", specifier = "~=0.1.12" },
|
||||||
{ name = "azure-ai-documentintelligence", specifier = ">=1.0.2" },
|
{ name = "azure-ai-documentintelligence", specifier = ">=1.0.2" },
|
||||||
{ name = "babel", specifier = ">=2.17" },
|
{ name = "babel", specifier = ">=2.17" },
|
||||||
{ name = "bleach", specifier = "~=6.3.0" },
|
{ name = "bleach", specifier = "~=6.3.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user