Compare commits

..

1 Commits

Author SHA1 Message Date
shamoon
17fba7da40 Add Coveralls for coverage 2025-09-26 09:17:53 -07:00
18 changed files with 1457 additions and 1887 deletions

View File

@@ -183,13 +183,23 @@ jobs:
if: always()
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }}
files: junit.xml
- name: Upload backend coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }}
files: coverage.xml
- name: Upload backend coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: coverage.xml
format: cobertura
flag-name: backend-python-${{ matrix.python-version }}
parallel: true
- name: Stop containers
if: always()
run: |
@@ -263,13 +273,35 @@ jobs:
uses: codecov/test-results-action@v1
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/
- name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/
- name: Upload frontend coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: src-ui/coverage/lcov.info
format: lcov
flag-name: frontend-node-${{ matrix.node-version }}-shard-${{ matrix.shard-index }}
parallel: true
coveralls-finish:
name: Finalize Coveralls
runs-on: ubuntu-24.04
needs:
- tests-backend
- tests-frontend
steps:
- name: Mark Coveralls jobs complete
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
tests-frontend-e2e:
name: "Frontend E2E Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04

View File

@@ -49,7 +49,7 @@ repos:
- 'prettier-plugin-organize-imports@4.1.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.2
rev: v0.13.0
hooks:
- id: ruff-check
- id: ruff-format
@@ -59,7 +59,7 @@ repos:
- id: pyproject-fmt
# Dockerfile hooks
- repo: https://github.com/AleksaC/hadolint-py
rev: v2.14.0
rev: v2.12.1b3
hooks:
- id: hadolint
# Shell script hooks

View File

@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.8.22-python3.12-bookworm-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.8.17-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6

View File

@@ -32,7 +32,7 @@ services:
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
image: docker.io/library/postgres:17
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data

View File

@@ -35,7 +35,7 @@ services:
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
image: docker.io/library/postgres:17
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data

View File

@@ -31,7 +31,7 @@ services:
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
image: docker.io/library/postgres:17
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data

View File

@@ -30,10 +30,10 @@ dependencies = [
"django-cachalot~=2.8.0",
"django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0",
"django-cors-headers~=4.8.0",
"django-extensions~=4.1",
"django-filter~=25.1",
"django-guardian~=3.2.0",
"django-guardian~=3.1.2",
"django-multiselectfield~=1.0.1",
"django-soft-delete~=1.0.18",
"django-treenode>=0.23.2",

View File

@@ -174,7 +174,7 @@ test('bulk edit', async ({ page }) => {
await expect(page.locator('pngx-document-list')).toHaveText(
/Selected 61 of 61 documents/i
)
await page.getByRole('button', { name: 'None' }).click()
await page.getByRole('button', { name: 'Cancel' }).click()
await page.locator('pngx-document-card-small').nth(1).click()
await page.locator('pngx-document-card-small').nth(2).click()

View File

@@ -324,7 +324,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">192</context>
<context context-type="linenumber">190</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -743,7 +743,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -1167,7 +1167,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">242</context>
<context context-type="linenumber">217</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
@@ -1209,7 +1209,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">78</context>
<context context-type="linenumber">97</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1494,6 +1494,10 @@
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">182</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
<context context-type="linenumber">81</context>
@@ -1600,10 +1604,6 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">153</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">4</context>
@@ -1755,7 +1755,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">269</context>
<context context-type="linenumber">244</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
@@ -1808,7 +1808,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">87</context>
<context context-type="linenumber">103</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -2109,7 +2109,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">140</context>
<context context-type="linenumber">157</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -2769,11 +2769,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">21</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">224</context>
<context context-type="linenumber">199</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -3001,7 +3001,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">129</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
@@ -3448,8 +3448,8 @@
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">30</context>
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="1496549861742963591" datatype="html">
@@ -3529,7 +3529,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">278</context>
<context context-type="linenumber">253</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
@@ -6356,7 +6356,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">323</context>
<context context-type="linenumber">298</context>
</context-group>
</trans-unit>
<trans-unit id="78870852467682010" datatype="html">
@@ -6371,7 +6371,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">363</context>
<context context-type="linenumber">338</context>
</context-group>
</trans-unit>
<trans-unit id="157572966557284263" datatype="html">
@@ -6386,7 +6386,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">370</context>
<context context-type="linenumber">345</context>
</context-group>
</trans-unit>
<trans-unit id="883965278435032344" datatype="html">
@@ -6404,7 +6404,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">391</context>
<context context-type="linenumber">366</context>
</context-group>
</trans-unit>
<trans-unit id="3542042671420335679" datatype="html">
@@ -6415,7 +6415,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">391</context>
<context context-type="linenumber">366</context>
</context-group>
</trans-unit>
<trans-unit id="872092479747931526" datatype="html">
@@ -6585,8 +6585,8 @@
<context context-type="linenumber">5</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">27</context>
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
</trans-unit>
<trans-unit id="2266163016683537825" datatype="html">
@@ -6625,7 +6625,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">91</context>
<context context-type="linenumber">107</context>
</context-group>
</trans-unit>
<trans-unit id="7049887240439736400" datatype="html">
@@ -6686,7 +6686,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">221</context>
<context context-type="linenumber">196</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@@ -6723,11 +6723,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">19</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">211</context>
<context context-type="linenumber">186</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -6750,11 +6750,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">33</context>
<context context-type="linenumber">49</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">251</context>
<context context-type="linenumber">226</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -6777,11 +6777,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">47</context>
<context context-type="linenumber">63</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">260</context>
<context context-type="linenumber">235</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7188,18 +7188,25 @@
<context context-type="linenumber">10</context>
</context-group>
</trans-unit>
<trans-unit id="6857598786757174736" datatype="html">
<source>Select:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="6299008920007331381" datatype="html">
<source>Edit:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">3</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="7001227209911602786" datatype="html">
<source>Filter tags</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">22</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7210,7 +7217,7 @@
<source>Filter correspondents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">20</context>
<context context-type="linenumber">36</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7221,7 +7228,7 @@
<source>Filter document types</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">50</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7232,7 +7239,7 @@
<source>Filter storage paths</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">64</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7243,7 +7250,7 @@
<source>Custom fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">77</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7258,56 +7265,56 @@
<source>Filter custom fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">62</context>
<context context-type="linenumber">78</context>
</context-group>
</trans-unit>
<trans-unit id="5139192806922838657" datatype="html">
<source>Set values</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit id="1050269006235116171" datatype="html">
<source>Rotate</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">94</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="3206542606001340679" datatype="html">
<source>Merge</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">97</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit id="1015374532025907183" datatype="html">
<source>Include:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">118</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit id="1537670659786159738" datatype="html">
<source>Archived files</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">122</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit id="2520291319362448498" datatype="html">
<source>Original files</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">143</context>
</context-group>
</trans-unit>
<trans-unit id="8009862506882713059" datatype="html">
<source>Use formatted filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">148</context>
</context-group>
</trans-unit>
<trans-unit id="1215215387232313677" datatype="html">
@@ -7607,7 +7614,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">339</context>
<context context-type="linenumber">314</context>
</context-group>
</trans-unit>
<trans-unit id="106713086593101376" datatype="html">
@@ -7731,7 +7738,7 @@
<source>Select</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">5</context>
<context context-type="linenumber">6</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context>
@@ -7742,51 +7749,36 @@
<source>Select none</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">11</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="1512866475468373520" datatype="html">
<source>Select page</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">12</context>
<context context-type="linenumber">10</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">315</context>
<context context-type="linenumber">313</context>
</context-group>
</trans-unit>
<trans-unit id="1494518490116523821" datatype="html">
<source>Select all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">13</context>
<context context-type="linenumber">11</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">308</context>
</context-group>
</trans-unit>
<trans-unit id="6252070156626006029" datatype="html">
<source>None</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">120</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
<context context-type="linenumber">45</context>
<context context-type="linenumber">306</context>
</context-group>
</trans-unit>
<trans-unit id="8461842260159597706" datatype="html">
<source>Show</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">37</context>
<context context-type="linenumber">17</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
@@ -7797,63 +7789,63 @@
<source>Sort</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">68</context>
<context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="2123659921722214537" datatype="html">
<source>Views</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">94</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="1233494216161906927" datatype="html">
<source>Save &quot;<x id="INTERPOLATION" equiv-text="{{list.activeSavedViewTitle}}"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">113</context>
<context context-type="linenumber">93</context>
</context-group>
</trans-unit>
<trans-unit id="2276119452079372898" datatype="html">
<source>Save as...</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="1450797155766668235" datatype="html">
<source>All saved views</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit id="8786996283897742947" datatype="html">
<source>{VAR_PLURAL, plural, =1 {Selected <x id="INTERPOLATION"/> of one document} other {Selected <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> documents}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">137</context>
<context context-type="linenumber">117</context>
</context-group>
</trans-unit>
<trans-unit id="6600548268163632449" datatype="html">
<source>{VAR_PLURAL, plural, =1 {One document} other {<x id="INTERPOLATION"/> documents}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">141</context>
<context context-type="linenumber">121</context>
</context-group>
</trans-unit>
<trans-unit id="2243770355958919528" datatype="html">
<source>(filtered)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">143</context>
<context context-type="linenumber">123</context>
</context-group>
</trans-unit>
<trans-unit id="6849725902312323996" datatype="html">
<source>Reset filters</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">148</context>
<context context-type="linenumber">128</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7864,21 +7856,21 @@
<source>Error while loading documents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">169</context>
<context context-type="linenumber">144</context>
</context-group>
</trans-unit>
<trans-unit id="494022736054110363" datatype="html">
<source>Sort by ASN</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">198</context>
<context context-type="linenumber">173</context>
</context-group>
</trans-unit>
<trans-unit id="7517688192215738656" datatype="html">
<source>ASN</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">202</context>
<context context-type="linenumber">177</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@@ -7897,28 +7889,28 @@
<source>Sort by correspondent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">207</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit id="2066713941761361709" datatype="html">
<source>Sort by title</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">216</context>
<context context-type="linenumber">191</context>
</context-group>
</trans-unit>
<trans-unit id="6232673011753681091" datatype="html">
<source>Sort by owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">204</context>
</context-group>
</trans-unit>
<trans-unit id="3715596725146409911" datatype="html">
<source>Owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">208</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
@@ -7933,49 +7925,49 @@
<source>Sort by notes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">238</context>
<context context-type="linenumber">213</context>
</context-group>
</trans-unit>
<trans-unit id="5499001829734502606" datatype="html">
<source>Sort by document type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">247</context>
<context context-type="linenumber">222</context>
</context-group>
</trans-unit>
<trans-unit id="6213829731736042759" datatype="html">
<source>Sort by storage path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">231</context>
</context-group>
</trans-unit>
<trans-unit id="3406167410329973166" datatype="html">
<source>Sort by created date</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">265</context>
<context context-type="linenumber">240</context>
</context-group>
</trans-unit>
<trans-unit id="3769035778779263084" datatype="html">
<source>Sort by added date</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">274</context>
<context context-type="linenumber">249</context>
</context-group>
</trans-unit>
<trans-unit id="4874754501044009042" datatype="html">
<source>Sort by number of pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">283</context>
<context context-type="linenumber">258</context>
</context-group>
</trans-unit>
<trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">287</context>
<context context-type="linenumber">262</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
@@ -7994,77 +7986,77 @@
<source> Shared </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">290,292</context>
<context context-type="linenumber">265,267</context>
</context-group>
</trans-unit>
<trans-unit id="5083658411133224968" datatype="html">
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">297,298</context>
<context context-type="linenumber">272,273</context>
</context-group>
</trans-unit>
<trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">331</context>
<context context-type="linenumber">306</context>
</context-group>
</trans-unit>
<trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">332</context>
<context context-type="linenumber">307</context>
</context-group>
</trans-unit>
<trans-unit id="4512084577073831437" datatype="html">
<source>Reset filters / selection</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">296</context>
<context context-type="linenumber">294</context>
</context-group>
</trans-unit>
<trans-unit id="4135055128446167640" datatype="html">
<source>Open first [selected] document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">324</context>
<context context-type="linenumber">322</context>
</context-group>
</trans-unit>
<trans-unit id="3629960544875360046" datatype="html">
<source>Previous page</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">340</context>
<context context-type="linenumber">338</context>
</context-group>
</trans-unit>
<trans-unit id="3337301694210287595" datatype="html">
<source>Next page</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">352</context>
<context context-type="linenumber">350</context>
</context-group>
</trans-unit>
<trans-unit id="2155249406916744630" datatype="html">
<source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">385</context>
<context context-type="linenumber">383</context>
</context-group>
</trans-unit>
<trans-unit id="4646273665293421938" datatype="html">
<source>Failed to save view &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot;.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">391</context>
<context context-type="linenumber">389</context>
</context-group>
</trans-unit>
<trans-unit id="6837554170707123455" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">437</context>
<context context-type="linenumber">435</context>
</context-group>
</trans-unit>
<trans-unit id="739880801667335279" datatype="html">
@@ -8869,6 +8861,17 @@
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="6252070156626006029" datatype="html">
<source>None</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">120</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
<context context-type="linenumber">45</context>
</context-group>
</trans-unit>
<trans-unit id="211408744872436427" datatype="html">
<source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">

View File

@@ -1,144 +1,161 @@
<div class="d-flex flex-wrap gap-4">
<div class="d-flex flex-wrap align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<label class="me-2" i18n>Edit:</label>
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createTag.bind(this)"
(opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
[documentCounts]="tagDocumentCounts"
(apply)="setTags($event)"
shortcutKey="t">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCorrespondent.bind(this)"
(opened)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel"
[documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)"
shortcutKey="y">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createDocumentType.bind(this)"
(opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)"
shortcutKey="u">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createStoragePath.bind(this)"
(opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)"
shortcutKey="i">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCustomField.bind(this)"
(opened)="openCustomFieldsDropdown()"
[(selectionModel)]="customFieldsSelectionModel"
[documentCounts]="customFieldDocumentCounts"
extraButtonTitle="Set values"
i18n-extraButtonTitle
(extraButton)="setCustomFieldValues($event)"
(apply)="setCustomFields($event)">
</pngx-filterable-dropdown>
}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
<div class="d-flex align-items-center" role="group" aria-label="Select">
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
<i-bs name="slash-circle"></i-bs>&nbsp;<ng-container i18n>Cancel</ng-container>
</button>
</div>
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-toolbar">
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" [disabled]="!userCanEdit && !userCanAdd" ngbDropdownToggle>
<i-bs name="three-dots"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="reprocessSelected()" [disabled]="!userCanEditAll && !userCanEditAll">
<i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Reprocess</ng-container>
<div class="d-flex align-items-center gap-2" role="group" aria-label="Select">
<label class="me-2" i18n>Select:</label>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<i-bs name="file-earmark-check"></i-bs>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll && !userCanEditAll">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanAdd || list.selected.size < 2">
<i-bs name="journals"></i-bs>&nbsp;<ng-container i18n>Merge</ng-container>
</button>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
@if (!awaitingDownload) {
<i-bs name="arrow-down"></i-bs>
}
@if (awaitingDownload) {
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span>
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
<i-bs name="check-all"></i-bs>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
}
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
</button>
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<form [formGroup]="downloadForm" class="px-3 py-1">
<p class="mb-1" i18n>Include:</p>
<div class="form-group ps-3 mb-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
<label class="form-check-label" for="downloadFileType_archive" i18n>Archived files</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
<label class="form-check-label" for="downloadFileType_originals" i18n>Original files</label>
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
<label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
</div>
</form>
</div>
</div>
</div>
<div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<label class="me-2" i18n>Edit:</label>
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createTag.bind(this)"
(opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
[documentCounts]="tagDocumentCounts"
(apply)="setTags($event)"
shortcutKey="t">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCorrespondent.bind(this)"
(opened)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel"
[documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)"
shortcutKey="y">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createDocumentType.bind(this)"
(opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)"
shortcutKey="u">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createStoragePath.bind(this)"
(opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)"
shortcutKey="i">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCustomField.bind(this)"
(opened)="openCustomFieldsDropdown()"
[(selectionModel)]="customFieldsSelectionModel"
[documentCounts]="customFieldDocumentCounts"
extraButtonTitle="Set values"
i18n-extraButtonTitle
(extraButton)="setCustomFieldValues($event)"
(apply)="setCustomFields($event)">
</pngx-filterable-dropdown>
}
</div>
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-toolbar">
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" [disabled]="!userCanEdit && !userCanAdd" ngbDropdownToggle>
<i-bs name="three-dots"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="reprocessSelected()" [disabled]="!userCanEditAll && !userCanEditAll">
<i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Reprocess</ng-container>
</button>
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll && !userCanEditAll">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanAdd || list.selected.size < 2">
<i-bs name="journals"></i-bs>&nbsp;<ng-container i18n>Merge</ng-container>
</button>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
@if (!awaitingDownload) {
<i-bs name="arrow-down"></i-bs>
}
@if (awaitingDownload) {
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span>
</div>
}
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
</button>
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<form [formGroup]="downloadForm" class="px-3 py-1">
<p class="mb-1" i18n>Include:</p>
<div class="form-group ps-3 mb-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
<label class="form-check-label" for="downloadFileType_archive" i18n>Archived files</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
<label class="form-check-label" for="downloadFileType_originals" i18n>Original files</label>
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
<label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
</div>
</form>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>

View File

@@ -5,7 +5,3 @@
.dropdown-menu{
--bs-dropdown-min-width: 12rem;
}
.btn-group .btn {
white-space: nowrap;
}

View File

@@ -1,13 +1,11 @@
<pngx-page-header [title]="getTitle()">
<div ngbDropdown class="btn-group flex-fill">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<i-bs name="text-indent-left"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Select</ng-container></div>
@if (list.selected.size > 0) {
<pngx-clearable-badge [selected]="list.selected.size > 0" [number]="list.selected.size" (cleared)="list.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
}
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
@@ -128,13 +126,8 @@
@if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
}
@if (!list.isReloading && list.selected.size > 0) {
<button class="btn btn-link py-0" (click)="list.selectNone()">
<i-bs width="1em" height="1em" name="slash-circle" class="me-1"></i-bs><small i18n>Clear selection</small>
</button>
}
</button>
}
</div>
@if (list.collectionSize) {
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"

View File

@@ -56,7 +56,6 @@ import {
filterRulesDiffer,
isFullTextFilterRule,
} from 'src/app/utils/filter-rules'
import { ClearableBadgeComponent } from '../common/clearable-badge/clearable-badge.component'
import { CustomFieldDisplayComponent } from '../common/custom-field-display/custom-field-display.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { PreviewPopupComponent } from '../common/preview-popup/preview-popup.component'
@@ -73,7 +72,6 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
templateUrl: './document-list.component.html',
styleUrls: ['./document-list.component.scss'],
imports: [
ClearableBadgeComponent,
CustomFieldDisplayComponent,
PageHeaderComponent,
BulkEditorComponent,

View File

@@ -6,7 +6,6 @@ import re
from datetime import datetime
from decimal import Decimal
from typing import TYPE_CHECKING
from typing import Literal
import magic
from celery import states
@@ -253,35 +252,6 @@ class OwnedObjectSerializer(
except KeyError:
pass
def _get_perms(self, obj, codename: str, target: Literal["users", "groups"]):
"""
Get the given permissions from context or from django-guardian.
:param codename: The permission codename, e.g. 'view' or 'change'
:param target: 'users' or 'groups'
"""
key = f"{target}_{codename}_perms"
cached = self.context.get(key, {}).get(obj.pk)
if cached is not None:
return list(cached)
# Permission not found in the context, get it from guardian
if target == "users":
return list(
get_users_with_perms(
obj,
only_with_perms_in=[f"{codename}_{obj.__class__.__name__.lower()}"],
with_group_users=False,
).values_list("id", flat=True),
)
else: # groups
return list(
get_groups_with_only_permission(
obj,
codename=f"{codename}_{obj.__class__.__name__.lower()}",
).values_list("id", flat=True),
)
@extend_schema_field(
field={
"type": "object",
@@ -316,14 +286,31 @@ class OwnedObjectSerializer(
},
)
def get_permissions(self, obj) -> dict:
view_codename = f"view_{obj.__class__.__name__.lower()}"
change_codename = f"change_{obj.__class__.__name__.lower()}"
return {
"view": {
"users": self._get_perms(obj, "view", "users"),
"groups": self._get_perms(obj, "view", "groups"),
"users": get_users_with_perms(
obj,
only_with_perms_in=[view_codename],
with_group_users=False,
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=view_codename,
).values_list("id", flat=True),
},
"change": {
"users": self._get_perms(obj, "change", "users"),
"groups": self._get_perms(obj, "change", "groups"),
"users": get_users_with_perms(
obj,
only_with_perms_in=[change_codename],
with_group_users=False,
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=change_codename,
).values_list("id", flat=True),
},
}

View File

@@ -1,23 +1,17 @@
import json
import tempfile
from datetime import timedelta
from pathlib import Path
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.db import connection
from django.test import TestCase
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import status
from documents.models import Document
from documents.models import ShareLink
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
from paperless.models import ApplicationConfiguration
@@ -160,113 +154,3 @@ class TestViews(DirectoriesMixin, TestCase):
response.render()
self.assertEqual(response.request["PATH_INFO"], "/accounts/login/")
self.assertContains(response, b"Share link has expired")
def test_list_with_full_permissions(self):
"""
GIVEN:
- Tags with different permissions
WHEN:
- Request to get tag list with full permissions is made
THEN:
- Tag list is returned with the right permission information
"""
user2 = User.objects.create(username="user2")
user3 = User.objects.create(username="user3")
group1 = Group.objects.create(name="group1")
group2 = Group.objects.create(name="group2")
group3 = Group.objects.create(name="group3")
t1 = Tag.objects.create(name="invoice", pk=1)
assign_perm("view_tag", self.user, t1)
assign_perm("view_tag", user2, t1)
assign_perm("view_tag", user3, t1)
assign_perm("view_tag", group1, t1)
assign_perm("view_tag", group2, t1)
assign_perm("view_tag", group3, t1)
assign_perm("change_tag", self.user, t1)
assign_perm("change_tag", user2, t1)
assign_perm("change_tag", group1, t1)
assign_perm("change_tag", group2, t1)
Tag.objects.create(name="bank statement", pk=2)
d1 = Document.objects.create(
title="Invoice 1",
content="This is the invoice of a very expensive item",
checksum="A",
)
d1.tags.add(t1)
d2 = Document.objects.create(
title="Invoice 2",
content="Internet invoice, I should pay it to continue contributing",
checksum="B",
)
d2.tags.add(t1)
view_permissions = Permission.objects.filter(
codename__contains="view_tag",
)
self.user.user_permissions.add(*view_permissions)
self.user.save()
self.client.force_login(self.user)
response = self.client.get("/api/tags/?page=1&full_perms=true")
results = json.loads(response.content)["results"]
for tag in results:
if tag["name"] == "invoice":
assert tag["permissions"] == {
"view": {
"users": [self.user.pk, user2.pk, user3.pk],
"groups": [group1.pk, group2.pk, group3.pk],
},
"change": {
"users": [self.user.pk, user2.pk],
"groups": [group1.pk, group2.pk],
},
}
elif tag["name"] == "bank statement":
assert tag["permissions"] == {
"view": {"users": [], "groups": []},
"change": {"users": [], "groups": []},
}
else:
assert False, f"Unexpected tag found: {tag['name']}"
def test_list_no_n_plus_1_queries(self):
"""
GIVEN:
- Tags with different permissions
WHEN:
- Request to get tag list with full permissions is made
THEN:
- Permissions are not queried in database tag by tag,
i.e. there are no N+1 queries
"""
view_permissions = Permission.objects.filter(
codename__contains="view_tag",
)
self.user.user_permissions.add(*view_permissions)
self.user.save()
self.client.force_login(self.user)
# Start by a small list, and count the number of SQL queries
for i in range(2):
Tag.objects.create(name=f"tag_{i}")
with CaptureQueriesContext(connection) as ctx_small:
response_small = self.client.get("/api/tags/?full_perms=true")
assert response_small.status_code == 200
num_queries_small = len(ctx_small.captured_queries)
# Complete the list, and count the number of SQL queries again
for i in range(2, 50):
Tag.objects.create(name=f"tag_{i}")
with CaptureQueriesContext(connection) as ctx_large:
response_large = self.client.get("/api/tags/?full_perms=true")
assert response_large.status_code == 200
num_queries_large = len(ctx_large.captured_queries)
# A few additional queries are allowed, but not a linear explosion
assert num_queries_large <= num_queries_small + 5, (
f"Possible N+1 queries detected: {num_queries_small} queries for 2 tags, "
f"but {num_queries_large} queries for 50 tags"
)

View File

@@ -5,11 +5,9 @@ import platform
import re
import tempfile
import zipfile
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from time import mktime
from typing import Literal
from unicodedata import normalize
from urllib.parse import quote
from urllib.parse import urlparse
@@ -21,7 +19,6 @@ from celery import states
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db import connections
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder
@@ -59,8 +56,6 @@ from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import extend_schema_view
from drf_spectacular.utils import inline_serializer
from guardian.utils import get_group_obj_perms_model
from guardian.utils import get_user_obj_perms_model
from langdetect import detect
from packaging import version as packaging_version
from redis import Redis
@@ -259,104 +254,7 @@ class PassUserMixin(GenericAPIView):
return super().get_serializer(*args, **kwargs)
class BulkPermissionMixin:
"""
Prefetch Django-Guardian permissions for a list before serialization, to avoid N+1 queries.
"""
def _get_object_perms(
self,
objects: list,
perm_codenames: list[str],
actor: Literal["users", "groups"],
) -> dict[int, dict[str, list[int]]]:
"""
Collect object-level permissions for either users or groups.
"""
model = self.queryset.model
obj_perm_model = (
get_user_obj_perms_model(model)
if actor == "users"
else get_group_obj_perms_model(model)
)
id_field = "user_id" if actor == "users" else "group_id"
ctype = ContentType.objects.get_for_model(model)
object_pks = [obj.pk for obj in objects]
perms_qs = obj_perm_model.objects.filter(
content_type=ctype,
object_pk__in=object_pks,
permission__codename__in=perm_codenames,
).values_list("object_pk", id_field, "permission__codename")
perms: dict[int, dict[str, list[int]]] = defaultdict(lambda: defaultdict(list))
for object_pk, actor_id, codename in perms_qs:
perms[int(object_pk)][codename].append(actor_id)
# Ensure that all objects have all codenames, even if empty
for pk in object_pks:
for codename in perm_codenames:
perms[pk][codename]
return perms
def get_serializer_context(self):
"""
Get all permissions of the current list of objects at once and pass them to the serializer.
This avoid fetching permissions object by object in database.
"""
context = super().get_serializer_context()
try:
full_perms = get_boolean(
str(self.request.query_params.get("full_perms", "false")),
)
except ValueError:
full_perms = False
if not full_perms:
return context
# Check which objects are being paginated
page = getattr(self, "paginator", None)
if page and hasattr(page, "page"):
queryset = page.page.object_list
elif hasattr(self, "page"):
queryset = self.page
else:
queryset = self.filter_queryset(self.get_queryset())
model_name = self.queryset.model.__name__.lower()
permission_name_view = f"view_{model_name}"
permission_name_change = f"change_{model_name}"
user_perms = self._get_object_perms(
objects=queryset,
perm_codenames=[permission_name_view, permission_name_change],
actor="users",
)
group_perms = self._get_object_perms(
objects=queryset,
perm_codenames=[permission_name_view, permission_name_change],
actor="groups",
)
context["users_view_perms"] = {
pk: user_perms[pk][permission_name_view] for pk in user_perms
}
context["users_change_perms"] = {
pk: user_perms[pk][permission_name_change] for pk in user_perms
}
context["groups_view_perms"] = {
pk: group_perms[pk][permission_name_view] for pk in group_perms
}
context["groups_change_perms"] = {
pk: group_perms[pk][permission_name_change] for pk in group_perms
}
return context
class PermissionsAwareDocumentCountMixin(BulkPermissionMixin, PassUserMixin):
class PermissionsAwareDocumentCountMixin(PassUserMixin):
"""
Mixin to add document count to queryset, permissions-aware if needed
"""

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-30 16:50+0000\n"
"POT-Creation-Date: 2025-09-22 18:20+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1191,44 +1191,44 @@ msgstr ""
msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:141
#: documents/serialisers.py:140
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:607
#: documents/serialisers.py:594
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:636
#: documents/serialisers.py:623
msgid "Invalid parent tag."
msgstr ""
#: documents/serialisers.py:1793
#: documents/serialisers.py:1780
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/serialisers.py:1837
#: documents/serialisers.py:1824
#, python-format
msgid "Custom field id must be an integer: %(id)s"
msgstr ""
#: documents/serialisers.py:1844
#: documents/serialisers.py:1831
#, python-format
msgid "Custom field with id %(id)s does not exist"
msgstr ""
#: documents/serialisers.py:1861 documents/serialisers.py:1871
#: documents/serialisers.py:1848 documents/serialisers.py:1858
msgid ""
"Custom fields must be a list of integers or an object mapping ids to values."
msgstr ""
#: documents/serialisers.py:1866
#: documents/serialisers.py:1853
msgid "Some custom fields don't exist or were specified twice."
msgstr ""
#: documents/serialisers.py:1936
#: documents/serialisers.py:1923
msgid "Invalid variable detected."
msgstr ""

2476
uv.lock generated

File diff suppressed because it is too large Load Diff