mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-09 23:49:29 -06:00
Compare commits
8 Commits
feature-do
...
chore/swit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59a25f8601 | ||
|
|
5950f22e87 | ||
|
|
40ff58ad39 | ||
|
|
914362224c | ||
|
|
e0b45539a6 | ||
|
|
c4ed4e7f36 | ||
|
|
0b89e2847e | ||
|
|
21623e4455 |
2
.github/workflows/ci-backend.yml
vendored
2
.github/workflows/ci-backend.yml
vendored
@@ -23,7 +23,7 @@ concurrency:
|
|||||||
group: backend-${{ github.event.pull_request.number || github.ref }}
|
group: backend-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.9.x"
|
DEFAULT_UV_VERSION: "0.10.x"
|
||||||
NLTK_DATA: "/usr/share/nltk_data"
|
NLTK_DATA: "/usr/share/nltk_data"
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
2
.github/workflows/ci-lint.yml
vendored
2
.github/workflows/ci-lint.yml
vendored
@@ -21,4 +21,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
- name: Run prek
|
- name: Run prek
|
||||||
uses: j178/prek-action@v1.1.0
|
uses: j178/prek-action@v1.1.1
|
||||||
|
|||||||
@@ -277,8 +277,6 @@ src/documents/management/commands/document_exporter.py:0: error: Skipping analyz
|
|||||||
src/documents/management/commands/document_exporter.py:0: error: Skipping analyzing "auditlog.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
|
src/documents/management/commands/document_exporter.py:0: error: Skipping analyzing "auditlog.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
|
||||||
src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def]
|
src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def]
|
||||||
src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def]
|
src/documents/management/commands/document_fuzzy_match.py:0: error: Function is missing a type annotation [no-untyped-def]
|
||||||
src/documents/management/commands/document_importer.py:0: error: Argument 1 to "create_source_path_directory" has incompatible type "Path | None"; expected "Path" [arg-type]
|
|
||||||
src/documents/management/commands/document_importer.py:0: error: Argument 2 to "copy_file_with_basic_stats" has incompatible type "Path | None"; expected "Path | str" [arg-type]
|
|
||||||
src/documents/management/commands/document_importer.py:0: error: Attribute "version" already defined on line 0 [no-redef]
|
src/documents/management/commands/document_importer.py:0: error: Attribute "version" already defined on line 0 [no-redef]
|
||||||
src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
||||||
src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
src/documents/management/commands/document_importer.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.9.29-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.10.0-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ dependencies = [
|
|||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"tika-client~=0.10.0",
|
"tika-client~=0.10.0",
|
||||||
"torch~=2.10.0",
|
"torch~=2.10.0",
|
||||||
"tqdm~=4.67.1",
|
|
||||||
"watchfiles>=1.1.1",
|
"watchfiles>=1.1.1",
|
||||||
"whitenoise~=6.11",
|
"whitenoise~=6.11",
|
||||||
"whoosh-reloaded>=2.7.5",
|
"whoosh-reloaded>=2.7.5",
|
||||||
@@ -150,7 +149,6 @@ typing = [
|
|||||||
"types-pytz",
|
"types-pytz",
|
||||||
"types-redis",
|
"types-redis",
|
||||||
"types-setuptools",
|
"types-setuptools",
|
||||||
"types-tqdm",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ test('dashboard saved view document links', async ({ page }) => {
|
|||||||
test('test slim sidebar', async ({ page }) => {
|
test('test slim sidebar', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.locator('.sidebar-slim-toggler').click()
|
await page.locator('#sidebarMenu').getByRole('button').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
await page.locator('.sidebar-slim-toggler').click()
|
await page.locator('#sidebarMenu').getByRole('button').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Correspondents' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/correspondents')
|
await page.goto('/correspondents')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -44,10 +44,8 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
test('should not allow user to view tags', async ({ page }) => {
|
test('should not allow user to view tags', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
await page.goto('/tags')
|
||||||
).not.toBeAttached()
|
|
||||||
await page.goto('/attributes/tags')
|
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -57,9 +55,9 @@ test('should not allow user to view document types', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Document Types' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/documenttypes')
|
await page.goto('/documenttypes')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -69,9 +67,9 @@ test('should not allow user to view storage paths', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
page.getByRole('link', { name: 'Storage Paths' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/storagepaths')
|
await page.goto('/storagepaths')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
|
|||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
import { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component'
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
|
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||||
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { MailComponent } from './components/manage/mail/mail.component'
|
import { MailComponent } from './components/manage/mail/mail.component'
|
||||||
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
|
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
|
||||||
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
@@ -101,77 +105,53 @@ export const routes: Routes = [
|
|||||||
componentName: 'DocumentAsnComponent',
|
componentName: 'DocumentAsnComponent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'attributes',
|
|
||||||
component: DocumentAttributesComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermissionAny: [
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.Tag },
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.Correspondent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
|
||||||
],
|
|
||||||
componentName: 'DocumentAttributesComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'attributes/:section',
|
|
||||||
component: DocumentAttributesComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermissionAny: [
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.Tag },
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.Correspondent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
|
||||||
],
|
|
||||||
componentName: 'DocumentAttributesComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'documentproperties',
|
|
||||||
redirectTo: '/attributes',
|
|
||||||
pathMatch: 'full',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'documentproperties/:section',
|
|
||||||
redirectTo: '/attributes/:section',
|
|
||||||
pathMatch: 'full',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'tags',
|
path: 'tags',
|
||||||
redirectTo: '/attributes/tags',
|
component: TagListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
},
|
data: {
|
||||||
{
|
requiredPermission: {
|
||||||
path: 'correspondents',
|
action: PermissionAction.View,
|
||||||
redirectTo: '/attributes/correspondents',
|
type: PermissionType.Tag,
|
||||||
pathMatch: 'full',
|
},
|
||||||
|
componentName: 'TagListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'documenttypes',
|
path: 'documenttypes',
|
||||||
redirectTo: '/attributes/documenttypes',
|
component: DocumentTypeListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
componentName: 'DocumentTypeListComponent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'correspondents',
|
||||||
|
component: CorrespondentListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Correspondent,
|
||||||
|
},
|
||||||
|
componentName: 'CorrespondentListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'storagepaths',
|
path: 'storagepaths',
|
||||||
redirectTo: '/attributes/storagepaths',
|
component: StoragePathListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.StoragePath,
|
||||||
|
},
|
||||||
|
componentName: 'StoragePathListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
@@ -259,8 +239,15 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'customfields',
|
path: 'customfields',
|
||||||
redirectTo: '/attributes/customfields',
|
component: CustomFieldsComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.CustomField,
|
||||||
|
},
|
||||||
|
componentName: 'CustomFieldsComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflows',
|
path: 'workflows',
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchorId: 'tour.tags',
|
anchorId: 'tour.tags',
|
||||||
content: $localize`Attributes like tags, correspondents, document types, storage paths and custom fields can all be managed here. They can also be created from the document edit view.`,
|
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
||||||
route: '/attributes/tags',
|
route: '/tags',
|
||||||
backdropConfig: {
|
backdropConfig: {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -175,60 +175,44 @@
|
|||||||
<span i18n>Manage</span>
|
<span i18n>Manage</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
@if (canManageAttributes) {
|
<li class="nav-item app-link"
|
||||||
<li class="nav-item app-link" tourAnchor="tour.tags">
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
<div class="d-flex align-items-center attributes-row">
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<a class="nav-link flex-fill" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()"
|
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
<i-bs class="me-1" name="stack"></i-bs><span> <ng-container i18n>Attributes</ng-container></span>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
@if (!slimSidebarEnabled) {
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
||||||
<button
|
tourAnchor="tour.tags">
|
||||||
type="button"
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
||||||
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||||
(click)="toggleAttributesSections($event)"
|
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
|
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
i18n-aria-label
|
</a>
|
||||||
>
|
</li>
|
||||||
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
|
<li class="nav-item app-link"
|
||||||
</button>
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
}
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
||||||
</div>
|
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
<div
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
class="attributes-submenu ms-2"
|
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
||||||
[ngbCollapse]="slimSidebarEnabled || attributesSectionsCollapsed"
|
</a>
|
||||||
>
|
</li>
|
||||||
<ul class="nav flex-column">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
|
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<a class="nav-link py-1" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
|
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
</a>
|
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
||||||
</li>
|
</a>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
</li>
|
||||||
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||||
</a>
|
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
</li>
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
||||||
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
</a>
|
||||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document types</ng-container></span>
|
</li>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
|
||||||
<a class="nav-link py-1" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
|
||||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage paths</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
|
||||||
<a class="nav-link py-1" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
|
|
||||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom fields</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
|
|||||||
@@ -177,15 +177,6 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attributes-row .attributes-expand-btn {
|
|
||||||
opacity: 0.2;
|
|
||||||
transition: opacity 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes-row:hover .attributes-expand-btn {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ import {
|
|||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import {
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
PermissionType,
|
|
||||||
PermissionsService,
|
|
||||||
} from 'src/app/services/permissions.service'
|
|
||||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
@@ -261,7 +258,7 @@ describe('AppFrameComponent', () => {
|
|||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
component.toggleSlimSidebar()
|
component.toggleSlimSidebar()
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.match(`${environment.apiBaseUrl}ui_settings/`)[0]
|
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||||
.flush('error', {
|
.flush('error', {
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: 'error',
|
statusText: 'error',
|
||||||
@@ -376,103 +373,4 @@ describe('AppFrameComponent', () => {
|
|||||||
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
||||||
expect(maybeRefreshSpy).toHaveBeenCalled()
|
expect(maybeRefreshSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should indicate attributes management availability when any permission is granted', () => {
|
|
||||||
jest
|
|
||||||
.spyOn(permissionsService, 'currentUserCan')
|
|
||||||
.mockImplementation((action, type) => {
|
|
||||||
return type === PermissionType.Tag
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(component.canManageAttributes).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should indicate attributes management availability for other permission types', () => {
|
|
||||||
const canSpy = jest
|
|
||||||
.spyOn(permissionsService, 'currentUserCan')
|
|
||||||
.mockImplementation((action, type) => {
|
|
||||||
return type === PermissionType.Correspondent
|
|
||||||
})
|
|
||||||
expect(component.canManageAttributes).toBe(true)
|
|
||||||
|
|
||||||
canSpy.mockImplementation((action, type) => {
|
|
||||||
return type === PermissionType.DocumentType
|
|
||||||
})
|
|
||||||
expect(component.canManageAttributes).toBe(true)
|
|
||||||
|
|
||||||
canSpy.mockImplementation((action, type) => {
|
|
||||||
return type === PermissionType.StoragePath
|
|
||||||
})
|
|
||||||
expect(component.canManageAttributes).toBe(true)
|
|
||||||
|
|
||||||
canSpy.mockImplementation((action, type) => {
|
|
||||||
return type === PermissionType.CustomField
|
|
||||||
})
|
|
||||||
expect(component.canManageAttributes).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should toggle attributes sections and stop event bubbling', () => {
|
|
||||||
const preventDefault = jest.fn()
|
|
||||||
const stopPropagation = jest.fn()
|
|
||||||
const setSpy = jest.spyOn(settingsService, 'set')
|
|
||||||
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
|
||||||
|
|
||||||
component.toggleAttributesSections({
|
|
||||||
preventDefault,
|
|
||||||
stopPropagation,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
expect(preventDefault).toHaveBeenCalled()
|
|
||||||
expect(stopPropagation).toHaveBeenCalled()
|
|
||||||
expect(setSpy).toHaveBeenCalledWith(
|
|
||||||
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
|
||||||
['attributes']
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should show error when saving slim sidebar setting fails', () => {
|
|
||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
|
||||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
||||||
jest
|
|
||||||
.spyOn(settingsService, 'storeSettings')
|
|
||||||
.mockReturnValue(throwError(() => new Error('boom')))
|
|
||||||
|
|
||||||
component.slimSidebarEnabled = true
|
|
||||||
|
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should show error when saving attributes collapsed setting fails', () => {
|
|
||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
|
||||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
||||||
jest
|
|
||||||
.spyOn(settingsService, 'storeSettings')
|
|
||||||
.mockReturnValue(throwError(() => new Error('boom')))
|
|
||||||
|
|
||||||
component.attributesSectionsCollapsed = true
|
|
||||||
|
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should persist attributes section collapse state', () => {
|
|
||||||
const setSpy = jest.spyOn(settingsService, 'set')
|
|
||||||
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
|
||||||
|
|
||||||
component.attributesSectionsCollapsed = true
|
|
||||||
|
|
||||||
expect(setSpy).toHaveBeenCalledWith(
|
|
||||||
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
|
||||||
['attributes']
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should collapse attributes sections when enabling slim sidebar', () => {
|
|
||||||
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
|
||||||
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
|
|
||||||
settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false)
|
|
||||||
|
|
||||||
component.toggleSlimSidebar()
|
|
||||||
|
|
||||||
expect(component.attributesSectionsCollapsed).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Observable } from 'rxjs'
|
|||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { CollapsibleSection, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||||
@@ -141,20 +141,11 @@ export class AppFrameComponent
|
|||||||
toggleSlimSidebar(): void {
|
toggleSlimSidebar(): void {
|
||||||
this.slimSidebarAnimating = true
|
this.slimSidebarAnimating = true
|
||||||
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
||||||
if (this.slimSidebarEnabled) {
|
|
||||||
this.attributesSectionsCollapsed = true
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.slimSidebarAnimating = false
|
this.slimSidebarAnimating = false
|
||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAttributesSections(event?: Event): void {
|
|
||||||
event?.preventDefault()
|
|
||||||
event?.stopPropagation()
|
|
||||||
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
get versionString(): string {
|
get versionString(): string {
|
||||||
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
||||||
}
|
}
|
||||||
@@ -176,31 +167,6 @@ export class AppFrameComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get canManageAttributes(): boolean {
|
|
||||||
return (
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
PermissionType.Tag
|
|
||||||
) ||
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
PermissionType.Correspondent
|
|
||||||
) ||
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
PermissionType.DocumentType
|
|
||||||
) ||
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
PermissionType.StoragePath
|
|
||||||
) ||
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
PermissionType.CustomField
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
@@ -220,31 +186,6 @@ export class AppFrameComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get attributesSectionsCollapsed(): boolean {
|
|
||||||
return this.settingsService
|
|
||||||
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
|
|
||||||
?.includes(CollapsibleSection.ATTRIBUTES)
|
|
||||||
}
|
|
||||||
|
|
||||||
set attributesSectionsCollapsed(collapsed: boolean) {
|
|
||||||
// TODO: refactor to be able to toggle individual sections, if implemented
|
|
||||||
this.settingsService.set(
|
|
||||||
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
|
||||||
collapsed ? [CollapsibleSection.ATTRIBUTES] : []
|
|
||||||
)
|
|
||||||
this.settingsService
|
|
||||||
.storeSettings()
|
|
||||||
.pipe(first())
|
|
||||||
.subscribe({
|
|
||||||
error: (error) => {
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`An error occurred while saving settings.`
|
|
||||||
)
|
|
||||||
console.warn(error)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { CorrespondentListComponent } from './correspondent-list.component'
|
import { CorrespondentListComponent } from './correspondent-list.component'
|
||||||
|
|
||||||
describe('CorrespondentListComponent', () => {
|
describe('CorrespondentListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { CorrespondentEditDialogComponent } from 'src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@@ -15,16 +14,21 @@ import { SortableDirective } from 'src/app/directives/sortable.directive'
|
|||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-correspondent-list',
|
selector: 'pngx-correspondent-list',
|
||||||
templateUrl: './../management-list.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
providers: [{ provide: CustomDatePipe }],
|
providers: [{ provide: CustomDatePipe }],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
@@ -33,10 +37,11 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
||||||
private readonly datePipe = inject(CustomDatePipe)
|
private datePipe = inject(CustomDatePipe)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<pngx-page-header
|
||||||
|
title="Custom Fields"
|
||||||
|
i18n-title
|
||||||
|
info="Customize the data fields that can be attached to documents."
|
||||||
|
i18n-info
|
||||||
|
infoLink="usage/#custom-fields"
|
||||||
|
>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
||||||
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
||||||
|
</button>
|
||||||
|
</pngx-page-header>
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
@@ -26,9 +26,9 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
|||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { CustomFieldEditDialogComponent } from '../../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { CustomFieldsComponent } from './custom-fields.component'
|
import { CustomFieldsComponent } from './custom-fields.component'
|
||||||
|
|
||||||
const fields: CustomField[] = [
|
const fields: CustomField[] = [
|
||||||
@@ -110,7 +110,10 @@ describe('CustomFieldsComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reload')
|
const reloadSpy = jest.spyOn(component, 'reload')
|
||||||
|
|
||||||
component.editField()
|
const createButton = fixture.debugElement
|
||||||
|
.queryAll(By.css('button'))
|
||||||
|
.find((btn) => btn.nativeElement.textContent.trim().includes('Add Field'))
|
||||||
|
createButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
|
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
|
||||||
@@ -7,10 +7,6 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { delay, takeUntil, tap } from 'rxjs'
|
import { delay, takeUntil, tap } from 'rxjs'
|
||||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
|
||||||
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
|
||||||
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
|
||||||
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
||||||
import {
|
import {
|
||||||
CustomFieldQueryLogicalOperator,
|
CustomFieldQueryLogicalOperator,
|
||||||
@@ -25,12 +21,18 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
|||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-custom-fields',
|
selector: 'pngx-custom-fields',
|
||||||
templateUrl: './custom-fields.component.html',
|
templateUrl: './custom-fields.component.html',
|
||||||
styleUrls: ['./custom-fields.component.scss'],
|
styleUrls: ['./custom-fields.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
@@ -42,14 +44,14 @@ export class CustomFieldsComponent
|
|||||||
extends LoadingComponentWithPermissions
|
extends LoadingComponentWithPermissions
|
||||||
implements OnInit
|
implements OnInit
|
||||||
{
|
{
|
||||||
private readonly customFieldsService = inject(CustomFieldsService)
|
private customFieldsService = inject(CustomFieldsService)
|
||||||
public readonly permissionsService = inject(PermissionsService)
|
permissionsService = inject(PermissionsService)
|
||||||
private readonly modalService = inject(NgbModal)
|
private modalService = inject(NgbModal)
|
||||||
private readonly toastService = inject(ToastService)
|
private toastService = inject(ToastService)
|
||||||
private readonly documentListViewService = inject(DocumentListViewService)
|
private documentListViewService = inject(DocumentListViewService)
|
||||||
private readonly settingsService = inject(SettingsService)
|
private settingsService = inject(SettingsService)
|
||||||
private readonly documentService = inject(DocumentService)
|
private documentService = inject(DocumentService)
|
||||||
private readonly savedViewService = inject(SavedViewService)
|
private savedViewService = inject(SavedViewService)
|
||||||
|
|
||||||
public fields: CustomField[] = []
|
public fields: CustomField[] = []
|
||||||
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<pngx-page-header
|
|
||||||
[title]="activeTabLabel"
|
|
||||||
info="Manage tags, correspondents, document types, storage paths, and custom fields."
|
|
||||||
i18n-info
|
|
||||||
[infoLink]="activeInfoLink"
|
|
||||||
[loading]="activeHeaderLoading"
|
|
||||||
>
|
|
||||||
@if (activeAttributeList) {
|
|
||||||
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
|
||||||
<i-bs name="text-indent-left"></i-bs>
|
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
|
||||||
@if (activeAttributeList.selectedObjects.size > 0) {
|
|
||||||
<pngx-clearable-badge [selected]="activeAttributeList.selectedObjects.size > 0" [number]="activeAttributeList.selectedObjects.size" (cleared)="activeAttributeList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
|
||||||
<button ngbDropdownItem (click)="activeAttributeList.selectNone()" i18n>Select none</button>
|
|
||||||
<button ngbDropdownItem (click)="activeAttributeList.selectPage(true)" i18n>Select page</button>
|
|
||||||
<button ngbDropdownItem (click)="activeAttributeList.selectAll()" i18n>Select all</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-none d-sm-flex flex-fill me-3">
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<span class="input-group-text border-0" i18n>Select:</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group btn-group-sm flex-nowrap">
|
|
||||||
@if (activeAttributeList.selectedObjects.size > 0) {
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="activeAttributeList.selectNone()">
|
|
||||||
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.selectPage(true)">
|
|
||||||
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.selectAll()">
|
|
||||||
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.setPermissions()"
|
|
||||||
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Change) || activeAttributeList.selectedObjects.size === 0">
|
|
||||||
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="activeAttributeList.delete()"
|
|
||||||
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Delete) || activeAttributeList.selectedObjects.size === 0">
|
|
||||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="activeAttributeList.openCreateDialog()"
|
|
||||||
*pngxIfPermissions="{ action: PermissionAction.Add, type: activeAttributeList.permissionType }">
|
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
|
||||||
</button>
|
|
||||||
} @else if (activeCustomFields) {
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeCustomFields.editField()"
|
|
||||||
*pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</pngx-page-header>
|
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-underline">
|
|
||||||
@for (section of visibleSections; track section.id) {
|
|
||||||
<li [ngbNavItem]="section.id">
|
|
||||||
<a ngbNavLink >
|
|
||||||
<i-bs class="me-2" [name]="section.icon"></i-bs>{{ section.label }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="my-3 shadow-sm">
|
|
||||||
<ng-container
|
|
||||||
[ngComponentOutlet]="activeSection?.component"
|
|
||||||
#activeOutlet="ngComponentOutlet"
|
|
||||||
></ng-container>
|
|
||||||
</div>
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import { Component } from '@angular/core'
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|
||||||
import {
|
|
||||||
ActivatedRoute,
|
|
||||||
convertToParamMap,
|
|
||||||
ParamMap,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router'
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subject } from 'rxjs'
|
|
||||||
import {
|
|
||||||
PermissionAction,
|
|
||||||
PermissionsService,
|
|
||||||
PermissionType,
|
|
||||||
} from 'src/app/services/permissions.service'
|
|
||||||
import { DocumentAttributesComponent } from './document-attributes.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'pngx-dummy-section',
|
|
||||||
template: '',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class DummySectionComponent {}
|
|
||||||
|
|
||||||
describe('DocumentAttributesComponent', () => {
|
|
||||||
let component: DocumentAttributesComponent
|
|
||||||
let fixture: ComponentFixture<DocumentAttributesComponent>
|
|
||||||
let router: Router
|
|
||||||
let paramMapSubject: Subject<ParamMap>
|
|
||||||
let permissionsService: PermissionsService
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
paramMapSubject = new Subject<ParamMap>()
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
RouterTestingModule,
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
DocumentAttributesComponent,
|
|
||||||
DummySectionComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: {
|
|
||||||
paramMap: paramMapSubject.asObservable(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: PermissionsService,
|
|
||||||
useValue: {
|
|
||||||
currentUserCan: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DocumentAttributesComponent)
|
|
||||||
component = fixture.componentInstance
|
|
||||||
router = TestBed.inject(Router)
|
|
||||||
permissionsService = TestBed.inject(PermissionsService)
|
|
||||||
|
|
||||||
jest.spyOn(router, 'navigate').mockResolvedValue(true)
|
|
||||||
;(component as any).sections = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
path: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
icon: 'tags',
|
|
||||||
permissionType: PermissionType.Tag,
|
|
||||||
kind: 'attributeList',
|
|
||||||
component: DummySectionComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
path: 'customfields',
|
|
||||||
label: 'Custom fields',
|
|
||||||
icon: 'ui-radios',
|
|
||||||
permissionType: PermissionType.CustomField,
|
|
||||||
kind: 'customFields',
|
|
||||||
component: DummySectionComponent,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should navigate to default section when no section is provided', () => {
|
|
||||||
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
|
||||||
(action, type) => {
|
|
||||||
return action === PermissionAction.View && type === PermissionType.Tag
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
fixture.detectChanges()
|
|
||||||
paramMapSubject.next(convertToParamMap({}))
|
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'tags'], {
|
|
||||||
replaceUrl: true,
|
|
||||||
})
|
|
||||||
expect(component.activeNavID).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should set active section from route param when valid', () => {
|
|
||||||
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
|
||||||
(action, type) => {
|
|
||||||
return (
|
|
||||||
action === PermissionAction.View &&
|
|
||||||
type === PermissionType.CustomField
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
fixture.detectChanges()
|
|
||||||
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
|
||||||
|
|
||||||
expect(component.activeNavID).toBe(2)
|
|
||||||
expect(router.navigate).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update active nav id when route section changes', () => {
|
|
||||||
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(true)
|
|
||||||
|
|
||||||
fixture.detectChanges()
|
|
||||||
component.activeNavID = 1
|
|
||||||
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
|
||||||
|
|
||||||
expect(component.activeNavID).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should redirect to dashboard when no sections are visible', () => {
|
|
||||||
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(false)
|
|
||||||
|
|
||||||
fixture.detectChanges()
|
|
||||||
paramMapSubject.next(convertToParamMap({}))
|
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/dashboard'], {
|
|
||||||
replaceUrl: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should navigate when a nav change occurs', () => {
|
|
||||||
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
|
|
||||||
fixture.detectChanges()
|
|
||||||
paramMapSubject.next(convertToParamMap({ section: 'tags' }))
|
|
||||||
|
|
||||||
component.onNavChange({ nextId: 2 } as any)
|
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'customfields'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should ignore nav changes for unknown sections', () => {
|
|
||||||
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(true)
|
|
||||||
|
|
||||||
fixture.detectChanges()
|
|
||||||
paramMapSubject.next(convertToParamMap({ section: 'tags' }))
|
|
||||||
|
|
||||||
component.onNavChange({ nextId: 999 } as any)
|
|
||||||
|
|
||||||
expect(router.navigate).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import { NgComponentOutlet } from '@angular/common'
|
|
||||||
import {
|
|
||||||
AfterViewChecked,
|
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
Type,
|
|
||||||
ViewChild,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
|
||||||
import {
|
|
||||||
NgbDropdownModule,
|
|
||||||
NgbNavChangeEvent,
|
|
||||||
NgbNavModule,
|
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
|
||||||
import {
|
|
||||||
PermissionAction,
|
|
||||||
PermissionsService,
|
|
||||||
PermissionType,
|
|
||||||
} from 'src/app/services/permissions.service'
|
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { CustomFieldsComponent } from './custom-fields/custom-fields.component'
|
|
||||||
import { CorrespondentListComponent } from './management-list/correspondent-list/correspondent-list.component'
|
|
||||||
import { DocumentTypeListComponent } from './management-list/document-type-list/document-type-list.component'
|
|
||||||
import { ManagementListComponent } from './management-list/management-list.component'
|
|
||||||
import { StoragePathListComponent } from './management-list/storage-path-list/storage-path-list.component'
|
|
||||||
import { TagListComponent } from './management-list/tag-list/tag-list.component'
|
|
||||||
|
|
||||||
enum DocumentAttributesNavIDs {
|
|
||||||
Tags = 1,
|
|
||||||
Correspondents = 2,
|
|
||||||
DocumentTypes = 3,
|
|
||||||
StoragePaths = 4,
|
|
||||||
CustomFields = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DocumentAttributesSectionKind {
|
|
||||||
ManagementList = 'managementList',
|
|
||||||
CustomFields = 'customFields',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentAttributesSection {
|
|
||||||
id: DocumentAttributesNavIDs
|
|
||||||
path: string
|
|
||||||
label: string
|
|
||||||
icon: string
|
|
||||||
infoLink?: string
|
|
||||||
permissionType: PermissionType
|
|
||||||
kind: DocumentAttributesSectionKind
|
|
||||||
component: Type<any>
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'pngx-document-attributes',
|
|
||||||
templateUrl: './document-attributes.component.html',
|
|
||||||
styleUrls: ['./document-attributes.component.scss'],
|
|
||||||
imports: [
|
|
||||||
PageHeaderComponent,
|
|
||||||
NgbNavModule,
|
|
||||||
NgbDropdownModule,
|
|
||||||
NgComponentOutlet,
|
|
||||||
NgxBootstrapIconsModule,
|
|
||||||
IfPermissionsDirective,
|
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class DocumentAttributesComponent
|
|
||||||
implements OnInit, OnDestroy, AfterViewChecked
|
|
||||||
{
|
|
||||||
private readonly permissionsService = inject(PermissionsService)
|
|
||||||
private readonly activatedRoute = inject(ActivatedRoute)
|
|
||||||
private readonly router = inject(Router)
|
|
||||||
private readonly cdr = inject(ChangeDetectorRef)
|
|
||||||
private readonly unsubscribeNotifier = new Subject<void>()
|
|
||||||
|
|
||||||
protected readonly PermissionAction = PermissionAction
|
|
||||||
protected readonly PermissionType = PermissionType
|
|
||||||
|
|
||||||
readonly sections: DocumentAttributesSection[] = [
|
|
||||||
{
|
|
||||||
id: DocumentAttributesNavIDs.Tags,
|
|
||||||
path: 'tags',
|
|
||||||
label: $localize`Tags`,
|
|
||||||
icon: 'tags',
|
|
||||||
infoLink: 'usage/#terms-and-definitions',
|
|
||||||
permissionType: PermissionType.Tag,
|
|
||||||
kind: DocumentAttributesSectionKind.ManagementList,
|
|
||||||
component: TagListComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: DocumentAttributesNavIDs.Correspondents,
|
|
||||||
path: 'correspondents',
|
|
||||||
label: $localize`Correspondents`,
|
|
||||||
icon: 'person',
|
|
||||||
infoLink: 'usage/#terms-and-definitions',
|
|
||||||
permissionType: PermissionType.Correspondent,
|
|
||||||
kind: DocumentAttributesSectionKind.ManagementList,
|
|
||||||
component: CorrespondentListComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: DocumentAttributesNavIDs.DocumentTypes,
|
|
||||||
path: 'documenttypes',
|
|
||||||
label: $localize`Document types`,
|
|
||||||
icon: 'hash',
|
|
||||||
infoLink: 'usage/#terms-and-definitions',
|
|
||||||
permissionType: PermissionType.DocumentType,
|
|
||||||
kind: DocumentAttributesSectionKind.ManagementList,
|
|
||||||
component: DocumentTypeListComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: DocumentAttributesNavIDs.StoragePaths,
|
|
||||||
path: 'storagepaths',
|
|
||||||
label: $localize`Storage paths`,
|
|
||||||
icon: 'folder',
|
|
||||||
infoLink: 'usage/#terms-and-definitions',
|
|
||||||
permissionType: PermissionType.StoragePath,
|
|
||||||
kind: DocumentAttributesSectionKind.ManagementList,
|
|
||||||
component: StoragePathListComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: DocumentAttributesNavIDs.CustomFields,
|
|
||||||
path: 'customfields',
|
|
||||||
label: $localize`Custom fields`,
|
|
||||||
icon: 'ui-radios',
|
|
||||||
infoLink: 'usage/#custom-fields',
|
|
||||||
permissionType: PermissionType.CustomField,
|
|
||||||
kind: DocumentAttributesSectionKind.CustomFields,
|
|
||||||
component: CustomFieldsComponent,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@ViewChild('activeOutlet', { read: NgComponentOutlet })
|
|
||||||
private readonly activeOutlet?: NgComponentOutlet
|
|
||||||
|
|
||||||
private lastHeaderLoading: boolean
|
|
||||||
|
|
||||||
activeNavID: number = null
|
|
||||||
|
|
||||||
get visibleSections(): DocumentAttributesSection[] {
|
|
||||||
return this.sections.filter((section) =>
|
|
||||||
this.permissionsService.currentUserCan(
|
|
||||||
PermissionAction.View,
|
|
||||||
section.permissionType
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeSection(): DocumentAttributesSection | null {
|
|
||||||
return (
|
|
||||||
this.visibleSections.find((section) => section.id === this.activeNavID) ??
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeAttributeList(): ManagementListComponent<any> | null {
|
|
||||||
if (
|
|
||||||
this.activeSection?.kind !== DocumentAttributesSectionKind.ManagementList
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
const instance = this.activeOutlet?.componentInstance
|
|
||||||
return instance instanceof ManagementListComponent ? instance : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeCustomFields(): CustomFieldsComponent | null {
|
|
||||||
if (this.activeSection?.kind !== DocumentAttributesSectionKind.CustomFields)
|
|
||||||
return null
|
|
||||||
const instance = this.activeOutlet?.componentInstance
|
|
||||||
return instance instanceof CustomFieldsComponent ? instance : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeTabLabel(): string {
|
|
||||||
return this.activeSection?.label ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeInfoLink(): string {
|
|
||||||
return this.activeSection?.infoLink ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeHeaderLoading(): boolean {
|
|
||||||
return (
|
|
||||||
this.activeAttributeList?.loading ??
|
|
||||||
this.activeCustomFields?.loading ??
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.activatedRoute.paramMap
|
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
||||||
.subscribe((paramMap) => {
|
|
||||||
const section = paramMap.get('section')
|
|
||||||
const navIDFromSection =
|
|
||||||
this.getNavIDForSection(section) ?? this.getDefaultNavID()
|
|
||||||
|
|
||||||
if (navIDFromSection == null) {
|
|
||||||
this.router.navigate(['/dashboard'], { replaceUrl: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.activeNavID !== navIDFromSection) {
|
|
||||||
this.activeNavID = navIDFromSection
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!section || this.getNavIDForSection(section) == null) {
|
|
||||||
this.router.navigate(
|
|
||||||
['attributes', this.getSectionForNavID(this.activeNavID)],
|
|
||||||
{ replaceUrl: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.unsubscribeNotifier.next()
|
|
||||||
this.unsubscribeNotifier.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewChecked(): void {
|
|
||||||
const current = this.activeHeaderLoading
|
|
||||||
if (this.lastHeaderLoading !== current) {
|
|
||||||
this.lastHeaderLoading = current
|
|
||||||
this.cdr.detectChanges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavChange(navChangeEvent: NgbNavChangeEvent): void {
|
|
||||||
const nextSection = this.getSectionForNavID(navChangeEvent.nextId)
|
|
||||||
if (!nextSection) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.router.navigate(['attributes', nextSection])
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDefaultNavID(): DocumentAttributesNavIDs | null {
|
|
||||||
return this.visibleSections[0]?.id ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNavIDForSection(section: string): DocumentAttributesNavIDs | null {
|
|
||||||
const path = section?.toLowerCase()
|
|
||||||
if (!path) return null
|
|
||||||
|
|
||||||
const found = this.visibleSections.find((s) => s.path === path)
|
|
||||||
return found?.id ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSectionForNavID(navID: number): string | null {
|
|
||||||
const section = this.visibleSections.find((s) => s.id === navID)
|
|
||||||
return section?.path ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { DocumentTypeListComponent } from './document-type-list.component'
|
import { DocumentTypeListComponent } from './document-type-list.component'
|
||||||
|
|
||||||
describe('DocumentTypeListComponent', () => {
|
describe('DocumentTypeListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,21 +7,25 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { DocumentTypeEditDialogComponent } from 'src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-document-type-list',
|
selector: 'pngx-document-type-list',
|
||||||
templateUrl: './../management-list.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
||||||
@@ -1,3 +1,50 @@
|
|||||||
|
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions" [loading]="loading">
|
||||||
|
|
||||||
|
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
||||||
|
<i-bs name="text-indent-left"></i-bs>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||||
|
@if (selectedObjects.size > 0) {
|
||||||
|
<pngx-clearable-badge [selected]="selectedObjects.size > 0" [number]="selectedObjects.size" (cleared)="selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
||||||
|
<button ngbDropdownItem (click)="selectNone()" i18n>Select none</button>
|
||||||
|
<button ngbDropdownItem (click)="selectPage(true)" i18n>Select page</button>
|
||||||
|
<button ngbDropdownItem (click)="selectAll()" i18n>Select all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none d-sm-flex flex-fill me-3">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text border-0" i18n>Select:</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm flex-nowrap">
|
||||||
|
@if (selectedObjects.size > 0) {
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="selectNone()">
|
||||||
|
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="selectPage(true)">
|
||||||
|
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="selectAll()">
|
||||||
|
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
||||||
|
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
|
||||||
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
||||||
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
||||||
|
</button>
|
||||||
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col mb-2 mb-xl-0">
|
<div class="col mb-2 mb-xl-0">
|
||||||
<div class="form-inline d-flex align-items-center">
|
<div class="form-inline d-flex align-items-center">
|
||||||
@@ -29,19 +76,19 @@
|
|||||||
<table class="table table-striped align-middle shadow-sm mb-0">
|
<table class="table table-striped align-middle shadow-sm mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th scope="col">
|
||||||
<div class="form-check m-0 ms-2 me-n2">
|
<div class="form-check m-0 ms-2 me-n2">
|
||||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (change)="$event.target.checked ? selectPage() : clearSelection(); $event.stopPropagation();">
|
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (change)="selectPage($event.target.checked); $event.stopPropagation();">
|
||||||
<label class="form-check-label" for="all-objects"></label>
|
<label class="form-check-label" for="all-objects"></label>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
<th scope="col" class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
<th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
<th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
@for (column of extraColumns; track column) {
|
@for (column of extraColumns; track column) {
|
||||||
<th class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
<th scope="col" class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||||
}
|
}
|
||||||
<th class="fw-normal" i18n>Actions</th>
|
<th scope="col" class="fw-normal" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -84,16 +131,16 @@
|
|||||||
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="name-cell" style="--depth: {{depth}}">
|
<td scope="row" class="name-cell" style="--depth: {{depth}}">
|
||||||
@if (depth > 0) {
|
@if (depth > 0) {
|
||||||
<div class="indicator"></div>
|
<div class="indicator"></div>
|
||||||
}
|
}
|
||||||
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||||
<td>{{ getDocumentCount(object) }}</td>
|
<td scope="row">{{ getDocumentCount(object) }}</td>
|
||||||
@for (column of extraColumns; track column) {
|
@for (column of extraColumns; track column) {
|
||||||
<td [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
<td scope="row" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
||||||
@if (column.badgeFn) {
|
@if (column.badgeFn) {
|
||||||
<span
|
<span
|
||||||
class="badge"
|
class="badge"
|
||||||
@@ -109,7 +156,7 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
<td>
|
<td scope="row">
|
||||||
<div class="btn-toolbar gap-2">
|
<div class="btn-toolbar gap-2">
|
||||||
<div class="btn-group d-block d-sm-none">
|
<div class="btn-group d-block d-sm-none">
|
||||||
<div ngbDropdown container="body" class="d-inline-block">
|
<div ngbDropdown container="body" class="d-inline-block">
|
||||||
@@ -44,12 +44,12 @@ import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-fil
|
|||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { EditDialogComponent } from '../../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { PermissionsDialogComponent } from '../../../common/permissions-dialog/permissions-dialog.component'
|
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { TagListComponent } from '../tag-list/tag-list.component'
|
||||||
import { ManagementListComponent } from './management-list.component'
|
import { ManagementListComponent } from './management-list.component'
|
||||||
import { TagListComponent } from './tag-list/tag-list.component'
|
|
||||||
|
|
||||||
const tags: Tag[] = [
|
const tags: Tag[] = [
|
||||||
{
|
{
|
||||||
@@ -304,12 +304,12 @@ describe('ManagementListComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('selectPage should select current page items or clear selection', () => {
|
it('selectPage should select current page items or clear selection', () => {
|
||||||
component.selectPage()
|
component.selectPage(true)
|
||||||
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
||||||
expect(component.togggleAll).toBe(true)
|
expect(component.togggleAll).toBe(true)
|
||||||
|
|
||||||
component.togggleAll = true
|
component.togggleAll = true
|
||||||
component.clearSelection()
|
component.selectPage(false)
|
||||||
expect(component.selectedObjects.size).toBe(0)
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
expect(component.togggleAll).toBe(false)
|
expect(component.togggleAll).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -16,10 +16,6 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs/operators'
|
} from 'rxjs/operators'
|
||||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
|
||||||
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
|
|
||||||
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
|
||||||
import {
|
import {
|
||||||
MATCH_AUTO,
|
MATCH_AUTO,
|
||||||
MATCH_NONE,
|
MATCH_NONE,
|
||||||
@@ -44,6 +40,10 @@ import {
|
|||||||
} from 'src/app/services/rest/abstract-name-filter-service'
|
} from 'src/app/services/rest/abstract-name-filter-service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
export interface ManagementListColumn {
|
export interface ManagementListColumn {
|
||||||
key: string
|
key: string
|
||||||
@@ -69,14 +69,13 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
protected service: AbstractNameFilterService<T>
|
protected service: AbstractNameFilterService<T>
|
||||||
private readonly modalService: NgbModal = inject(NgbModal)
|
private modalService: NgbModal = inject(NgbModal)
|
||||||
protected editDialogComponent: any
|
protected editDialogComponent: any
|
||||||
private readonly toastService: ToastService = inject(ToastService)
|
private toastService: ToastService = inject(ToastService)
|
||||||
private readonly documentListViewService: DocumentListViewService = inject(
|
private documentListViewService: DocumentListViewService = inject(
|
||||||
DocumentListViewService
|
DocumentListViewService
|
||||||
)
|
)
|
||||||
private readonly permissionsService: PermissionsService =
|
private permissionsService: PermissionsService = inject(PermissionsService)
|
||||||
inject(PermissionsService)
|
|
||||||
protected filterRuleType: number
|
protected filterRuleType: number
|
||||||
public typeName: string
|
public typeName: string
|
||||||
public typeNamePlural: string
|
public typeNamePlural: string
|
||||||
@@ -197,7 +196,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
openCreateDialog() {
|
openCreateDialog() {
|
||||||
const activeModal = this.modalService.open(this.editDialogComponent, {
|
var activeModal = this.modalService.open(this.editDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
|
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||||
@@ -216,7 +215,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
openEditDialog(object: T) {
|
openEditDialog(object: T) {
|
||||||
const activeModal = this.modalService.open(this.editDialogComponent, {
|
var activeModal = this.modalService.open(this.editDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.object = object
|
activeModal.componentInstance.object = object
|
||||||
@@ -244,7 +243,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
openDeleteDialog(object: T) {
|
openDeleteDialog(object: T) {
|
||||||
const activeModal = this.modalService.open(ConfirmDialogComponent, {
|
var activeModal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.title = $localize`Confirm delete`
|
activeModal.componentInstance.title = $localize`Confirm delete`
|
||||||
@@ -344,9 +343,13 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
selectPage() {
|
selectPage(select: boolean) {
|
||||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
if (select) {
|
||||||
this.togggleAll = this.areAllPageItemsSelected()
|
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||||
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
|
} else {
|
||||||
|
this.clearSelection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAll() {
|
selectAll() {
|
||||||
@@ -10,7 +10,7 @@ import { StoragePath } from 'src/app/data/storage-path'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { StoragePathListComponent } from './storage-path-list.component'
|
import { StoragePathListComponent } from './storage-path-list.component'
|
||||||
|
|
||||||
describe('StoragePathListComponent', () => {
|
describe('StoragePathListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,21 +7,25 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { StoragePathEditDialogComponent } from 'src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
|
||||||
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-storage-path-list',
|
selector: 'pngx-storage-path-list',
|
||||||
templateUrl: './../management-list.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { TagListComponent } from './tag-list.component'
|
import { TagListComponent } from './tag-list.component'
|
||||||
|
|
||||||
describe('TagListComponent', () => {
|
describe('TagListComponent', () => {
|
||||||
@@ -138,12 +138,12 @@ describe('TagListComponent', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component.data = [parent as any]
|
component.data = [parent as any]
|
||||||
component.selectPage()
|
component.selectPage(true)
|
||||||
|
|
||||||
expect(component.selectedObjects.has(10)).toBe(true)
|
expect(component.selectedObjects.has(10)).toBe(true)
|
||||||
expect(component.selectedObjects.has(11)).toBe(true)
|
expect(component.selectedObjects.has(11)).toBe(true)
|
||||||
|
|
||||||
component.clearSelection()
|
component.selectPage(false)
|
||||||
expect(component.selectedObjects.size).toBe(0)
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,21 +7,25 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { TagEditDialogComponent } from 'src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
|
||||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
|
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-tag-list',
|
selector: 'pngx-tag-list',
|
||||||
templateUrl: './../management-list.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TagListComponent extends ManagementListComponent<Tag> {
|
export class TagListComponent extends ManagementListComponent<Tag> {
|
||||||
@@ -19,10 +19,6 @@ export enum GlobalSearchType {
|
|||||||
TITLE_CONTENT = 'title-content',
|
TITLE_CONTENT = 'title-content',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CollapsibleSection {
|
|
||||||
ATTRIBUTES = 'attributes',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PAPERLESS_GREEN_HEX = '#17541f'
|
export const PAPERLESS_GREEN_HEX = '#17541f'
|
||||||
|
|
||||||
export const SETTINGS_KEYS = {
|
export const SETTINGS_KEYS = {
|
||||||
@@ -55,8 +51,6 @@ export const SETTINGS_KEYS = {
|
|||||||
NOTES_ENABLED: 'general-settings:notes-enabled',
|
NOTES_ENABLED: 'general-settings:notes-enabled',
|
||||||
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
||||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||||
ATTRIBUTES_SECTIONS_COLLAPSED:
|
|
||||||
'general-settings:attributes-sections-collapsed',
|
|
||||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||||
UPDATE_CHECKING_BACKEND_SETTING:
|
UPDATE_CHECKING_BACKEND_SETTING:
|
||||||
'general-settings:update-checking:backend-setting',
|
'general-settings:update-checking:backend-setting',
|
||||||
@@ -118,11 +112,6 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
|
||||||
type: 'array',
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
|||||||
@@ -96,52 +96,4 @@ describe('PermissionsGuard', () => {
|
|||||||
expect(canActivate).toHaveProperty('root') // returns UrlTree
|
expect(canActivate).toHaveProperty('root') // returns UrlTree
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should activate when any required permission is granted', () => {
|
|
||||||
jest
|
|
||||||
.spyOn(permissionsService, 'currentUserCan')
|
|
||||||
.mockImplementation((action, type) => {
|
|
||||||
return type === PermissionType.Tag
|
|
||||||
})
|
|
||||||
|
|
||||||
const canActivate = guard.canActivate(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
requiredPermissionAny: [
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.Tag },
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
routerState.snapshot
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(canActivate).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not activate when no required permission is granted', () => {
|
|
||||||
jest
|
|
||||||
.spyOn(permissionsService, 'currentUserCan')
|
|
||||||
.mockImplementation(() => false)
|
|
||||||
|
|
||||||
const canActivate = guard.canActivate(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
requiredPermissionAny: [
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.Tag },
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
routerState.snapshot
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(canActivate).toHaveProperty('root')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,20 +20,12 @@ export class PermissionsGuard {
|
|||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot
|
state: RouterStateSnapshot
|
||||||
): boolean | UrlTree {
|
): boolean | UrlTree {
|
||||||
const requiredPermissionAny: { action: any; type: any }[] =
|
|
||||||
route.data.requiredPermissionAny
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
||||||
(route.data.requiredPermission &&
|
(route.data.requiredPermission &&
|
||||||
!this.permissionsService.currentUserCan(
|
!this.permissionsService.currentUserCan(
|
||||||
route.data.requiredPermission.action,
|
route.data.requiredPermission.action,
|
||||||
route.data.requiredPermission.type
|
route.data.requiredPermission.type
|
||||||
)) ||
|
|
||||||
(Array.isArray(requiredPermissionAny) &&
|
|
||||||
requiredPermissionAny.length > 0 &&
|
|
||||||
!requiredPermissionAny.some((p) =>
|
|
||||||
this.permissionsService.currentUserCan(p.action, p.type)
|
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
// Check if tour is running 1 = TourState.ON
|
// Check if tour is running 1 = TourState.ON
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ import {
|
|||||||
sliders2Vertical,
|
sliders2Vertical,
|
||||||
sortAlphaDown,
|
sortAlphaDown,
|
||||||
sortAlphaUpAlt,
|
sortAlphaUpAlt,
|
||||||
stack,
|
|
||||||
stars,
|
stars,
|
||||||
tag,
|
tag,
|
||||||
tagFill,
|
tagFill,
|
||||||
@@ -344,7 +343,6 @@ const icons = {
|
|||||||
sliders2Vertical,
|
sliders2Vertical,
|
||||||
sortAlphaDown,
|
sortAlphaDown,
|
||||||
sortAlphaUpAlt,
|
sortAlphaUpAlt,
|
||||||
stack,
|
|
||||||
stars,
|
stars,
|
||||||
tagFill,
|
tagFill,
|
||||||
tag,
|
tag,
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from django import db
|
from django import db
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.management.commands.mixins import MultiProcessMixin
|
from documents.management.commands.mixins import MultiProcessMixin
|
||||||
from documents.management.commands.mixins import ProgressBarMixin
|
from documents.management.commands.mixins import ProgressBarMixin
|
||||||
@@ -75,20 +79,24 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
|
|||||||
try:
|
try:
|
||||||
logging.getLogger().handlers[0].level = logging.ERROR
|
logging.getLogger().handlers[0].level = logging.ERROR
|
||||||
|
|
||||||
if self.process_count == 1:
|
with Progress(
|
||||||
for doc_id in document_ids:
|
TextColumn("[progress.description]{task.description}"),
|
||||||
update_document_content_maybe_archive_file(doc_id)
|
BarColumn(),
|
||||||
else: # pragma: no cover
|
TaskProgressColumn(),
|
||||||
with multiprocessing.Pool(self.process_count) as pool:
|
TimeRemainingColumn(),
|
||||||
list(
|
disable=self.no_progress_bar,
|
||||||
tqdm.tqdm(
|
) as progress:
|
||||||
pool.imap_unordered(
|
task = progress.add_task("Archiving documents", total=len(document_ids))
|
||||||
update_document_content_maybe_archive_file,
|
if self.process_count == 1:
|
||||||
document_ids,
|
for doc_id in document_ids:
|
||||||
),
|
update_document_content_maybe_archive_file(doc_id)
|
||||||
total=len(document_ids),
|
progress.update(task, advance=1)
|
||||||
disable=self.no_progress_bar,
|
else: # pragma: no cover
|
||||||
),
|
with multiprocessing.Pool(self.process_count) as pool:
|
||||||
)
|
for _ in pool.imap_unordered(
|
||||||
|
update_document_content_maybe_archive_file,
|
||||||
|
document_ids,
|
||||||
|
):
|
||||||
|
progress.update(task, advance=1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
self.stdout.write(self.style.NOTICE("Aborting..."))
|
self.stdout.write(self.style.NOTICE("Aborting..."))
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from allauth.mfa.models import Authenticator
|
from allauth.mfa.models import Authenticator
|
||||||
from allauth.socialaccount.models import SocialAccount
|
from allauth.socialaccount.models import SocialAccount
|
||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
@@ -24,6 +23,11 @@ from django.utils import timezone
|
|||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
from guardian.models import GroupObjectPermission
|
from guardian.models import GroupObjectPermission
|
||||||
from guardian.models import UserObjectPermission
|
from guardian.models import UserObjectPermission
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
@@ -309,54 +313,64 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
document_manifest = manifest_dict["documents"]
|
document_manifest = manifest_dict["documents"]
|
||||||
|
|
||||||
# 3. Export files from each document
|
# 3. Export files from each document
|
||||||
for index, document_dict in tqdm.tqdm(
|
with Progress(
|
||||||
enumerate(document_manifest),
|
TextColumn("[progress.description]{task.description}"),
|
||||||
total=len(document_manifest),
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
disable=self.no_progress_bar,
|
disable=self.no_progress_bar,
|
||||||
):
|
) as progress:
|
||||||
document = document_map[document_dict["pk"]]
|
task = progress.add_task(
|
||||||
|
"Exporting documents",
|
||||||
# 3.1. generate a unique filename
|
total=len(document_manifest),
|
||||||
base_name = self.generate_base_name(document)
|
|
||||||
|
|
||||||
# 3.2. write filenames into manifest
|
|
||||||
original_target, thumbnail_target, archive_target = (
|
|
||||||
self.generate_document_targets(document, base_name, document_dict)
|
|
||||||
)
|
)
|
||||||
|
for index, document_dict in enumerate(document_manifest):
|
||||||
|
document = document_map[document_dict["pk"]]
|
||||||
|
|
||||||
# 3.3. write files to target folder
|
# 3.1. generate a unique filename
|
||||||
if not self.data_only:
|
base_name = self.generate_base_name(document)
|
||||||
self.copy_document_files(
|
|
||||||
document,
|
# 3.2. write filenames into manifest
|
||||||
original_target,
|
original_target, thumbnail_target, archive_target = (
|
||||||
thumbnail_target,
|
self.generate_document_targets(document, base_name, document_dict)
|
||||||
archive_target,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.split_manifest:
|
# 3.3. write files to target folder
|
||||||
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
|
if not self.data_only:
|
||||||
if self.use_folder_prefix:
|
self.copy_document_files(
|
||||||
manifest_name = Path("json") / manifest_name
|
document,
|
||||||
manifest_name = (self.target / manifest_name).resolve()
|
original_target,
|
||||||
manifest_name.parent.mkdir(parents=True, exist_ok=True)
|
thumbnail_target,
|
||||||
content = [document_manifest[index]]
|
archive_target,
|
||||||
content += list(
|
)
|
||||||
filter(
|
|
||||||
lambda d: d["fields"]["document"] == document_dict["pk"],
|
|
||||||
manifest_dict["notes"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
content += list(
|
|
||||||
filter(
|
|
||||||
lambda d: d["fields"]["document"] == document_dict["pk"],
|
|
||||||
manifest_dict["custom_field_instances"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.check_and_write_json(
|
if self.split_manifest:
|
||||||
content,
|
manifest_name = base_name.with_name(
|
||||||
manifest_name,
|
f"{base_name.stem}-manifest.json",
|
||||||
)
|
)
|
||||||
|
if self.use_folder_prefix:
|
||||||
|
manifest_name = Path("json") / manifest_name
|
||||||
|
manifest_name = (self.target / manifest_name).resolve()
|
||||||
|
manifest_name.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
content = [document_manifest[index]]
|
||||||
|
content += list(
|
||||||
|
filter(
|
||||||
|
lambda d: d["fields"]["document"] == document_dict["pk"],
|
||||||
|
manifest_dict["notes"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
content += list(
|
||||||
|
filter(
|
||||||
|
lambda d: d["fields"]["document"] == document_dict["pk"],
|
||||||
|
manifest_dict["custom_field_instances"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.check_and_write_json(
|
||||||
|
content,
|
||||||
|
manifest_name,
|
||||||
|
)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|
||||||
# These were exported already
|
# These were exported already
|
||||||
if self.split_manifest:
|
if self.split_manifest:
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import multiprocessing
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
import rapidfuzz
|
import rapidfuzz
|
||||||
import tqdm
|
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.core.management import CommandError
|
from django.core.management import CommandError
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.management.commands.mixins import MultiProcessMixin
|
from documents.management.commands.mixins import MultiProcessMixin
|
||||||
from documents.management.commands.mixins import ProgressBarMixin
|
from documents.management.commands.mixins import ProgressBarMixin
|
||||||
@@ -106,19 +110,25 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
|
|||||||
work_pkgs.append(_WorkPackage(first_doc, second_doc))
|
work_pkgs.append(_WorkPackage(first_doc, second_doc))
|
||||||
|
|
||||||
# Don't spin up a pool of 1 process
|
# Don't spin up a pool of 1 process
|
||||||
if self.process_count == 1:
|
with Progress(
|
||||||
results = []
|
TextColumn("[progress.description]{task.description}"),
|
||||||
for work in tqdm.tqdm(work_pkgs, disable=self.no_progress_bar):
|
BarColumn(),
|
||||||
results.append(_process_and_match(work))
|
TaskProgressColumn(),
|
||||||
else: # pragma: no cover
|
TimeRemainingColumn(),
|
||||||
with multiprocessing.Pool(processes=self.process_count) as pool:
|
disable=self.no_progress_bar,
|
||||||
results = list(
|
) as progress:
|
||||||
tqdm.tqdm(
|
task = progress.add_task("Fuzzy matching documents", total=len(work_pkgs))
|
||||||
pool.imap_unordered(_process_and_match, work_pkgs),
|
if self.process_count == 1:
|
||||||
total=len(work_pkgs),
|
results = []
|
||||||
disable=self.no_progress_bar,
|
for work in work_pkgs:
|
||||||
),
|
results.append(_process_and_match(work))
|
||||||
)
|
progress.update(task, advance=1)
|
||||||
|
else: # pragma: no cover
|
||||||
|
with multiprocessing.Pool(processes=self.process_count) as pool:
|
||||||
|
results = []
|
||||||
|
for result in pool.imap_unordered(_process_and_match, work_pkgs):
|
||||||
|
results.append(result)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|
||||||
# Check results
|
# Check results
|
||||||
messages = []
|
messages = []
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import tempfile
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from zipfile import is_zipfile
|
from zipfile import is_zipfile
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@@ -23,6 +23,11 @@ from django.db import transaction
|
|||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.management.commands.mixins import CryptMixin
|
from documents.management.commands.mixins import CryptMixin
|
||||||
@@ -365,58 +370,72 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
filter(lambda r: r["model"] == "documents.document", self.manifest),
|
filter(lambda r: r["model"] == "documents.document", self.manifest),
|
||||||
)
|
)
|
||||||
|
|
||||||
for record in tqdm.tqdm(manifest_documents, disable=self.no_progress_bar):
|
with Progress(
|
||||||
document = Document.objects.get(pk=record["pk"])
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
disable=self.no_progress_bar,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task(
|
||||||
|
"Importing documents",
|
||||||
|
total=len(manifest_documents),
|
||||||
|
)
|
||||||
|
for record in manifest_documents:
|
||||||
|
document = Document.objects.get(pk=record["pk"])
|
||||||
|
|
||||||
doc_file = record[EXPORTER_FILE_NAME]
|
doc_file = record[EXPORTER_FILE_NAME]
|
||||||
document_path = self.source / doc_file
|
document_path = self.source / doc_file
|
||||||
|
|
||||||
if EXPORTER_THUMBNAIL_NAME in record:
|
if EXPORTER_THUMBNAIL_NAME in record:
|
||||||
thumb_file = record[EXPORTER_THUMBNAIL_NAME]
|
thumb_file = record[EXPORTER_THUMBNAIL_NAME]
|
||||||
thumbnail_path = (self.source / thumb_file).resolve()
|
thumbnail_path = (self.source / thumb_file).resolve()
|
||||||
else:
|
else:
|
||||||
thumbnail_path = None
|
thumbnail_path = None
|
||||||
|
|
||||||
if EXPORTER_ARCHIVE_NAME in record:
|
if EXPORTER_ARCHIVE_NAME in record:
|
||||||
archive_file = record[EXPORTER_ARCHIVE_NAME]
|
archive_file = record[EXPORTER_ARCHIVE_NAME]
|
||||||
archive_path = self.source / archive_file
|
archive_path = self.source / archive_file
|
||||||
else:
|
else:
|
||||||
archive_path = None
|
archive_path = None
|
||||||
|
|
||||||
with FileLock(settings.MEDIA_LOCK):
|
with FileLock(settings.MEDIA_LOCK):
|
||||||
if Path(document.source_path).is_file():
|
if Path(document.source_path).is_file():
|
||||||
raise FileExistsError(document.source_path)
|
raise FileExistsError(document.source_path)
|
||||||
|
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
|
|
||||||
copy_file_with_basic_stats(document_path, document.source_path)
|
copy_file_with_basic_stats(document_path, document.source_path)
|
||||||
|
|
||||||
if thumbnail_path:
|
if thumbnail_path:
|
||||||
if thumbnail_path.suffix in {".png", ".PNG"}:
|
if thumbnail_path.suffix in {".png", ".PNG"}:
|
||||||
run_convert(
|
run_convert(
|
||||||
density=300,
|
density=300,
|
||||||
scale="500x5000>",
|
scale="500x5000>",
|
||||||
alpha="remove",
|
alpha="remove",
|
||||||
strip=True,
|
strip=True,
|
||||||
trim=False,
|
trim=False,
|
||||||
auto_orient=True,
|
auto_orient=True,
|
||||||
input_file=f"{thumbnail_path}[0]",
|
input_file=f"{thumbnail_path}[0]",
|
||||||
output_file=str(document.thumbnail_path),
|
output_file=str(document.thumbnail_path),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
copy_file_with_basic_stats(
|
copy_file_with_basic_stats(
|
||||||
thumbnail_path,
|
thumbnail_path,
|
||||||
document.thumbnail_path,
|
document.thumbnail_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if archive_path:
|
if archive_path:
|
||||||
create_source_path_directory(document.archive_path)
|
if TYPE_CHECKING:
|
||||||
# TODO: this assumes that the export is valid and
|
assert document.archive_path is not None
|
||||||
# archive_filename is present on all documents with
|
create_source_path_directory(document.archive_path)
|
||||||
# archived files
|
# TODO: this assumes that the export is valid and
|
||||||
copy_file_with_basic_stats(archive_path, document.archive_path)
|
# archive_filename is present on all documents with
|
||||||
|
# archived files
|
||||||
|
copy_file_with_basic_stats(archive_path, document.archive_path)
|
||||||
|
|
||||||
document.save()
|
document.save()
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|
||||||
def decrypt_secret_fields(self) -> None:
|
def decrypt_secret_fields(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.management.commands.mixins import ProgressBarMixin
|
from documents.management.commands.mixins import ProgressBarMixin
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
@@ -18,8 +22,15 @@ class Command(ProgressBarMixin, BaseCommand):
|
|||||||
self.handle_progress_bar_mixin(**options)
|
self.handle_progress_bar_mixin(**options)
|
||||||
logging.getLogger().handlers[0].level = logging.ERROR
|
logging.getLogger().handlers[0].level = logging.ERROR
|
||||||
|
|
||||||
for document in tqdm.tqdm(
|
documents = Document.objects.all()
|
||||||
Document.objects.all(),
|
with Progress(
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
disable=self.no_progress_bar,
|
disable=self.no_progress_bar,
|
||||||
):
|
) as progress:
|
||||||
post_save.send(Document, instance=document, created=False)
|
task = progress.add_task("Renaming documents", total=documents.count())
|
||||||
|
for document in documents:
|
||||||
|
post_save.send(Document, instance=document, created=False)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
from documents.management.commands.mixins import ProgressBarMixin
|
from documents.management.commands.mixins import ProgressBarMixin
|
||||||
@@ -84,53 +88,62 @@ class Command(ProgressBarMixin, BaseCommand):
|
|||||||
|
|
||||||
classifier = load_classifier()
|
classifier = load_classifier()
|
||||||
|
|
||||||
for document in tqdm.tqdm(documents, disable=self.no_progress_bar):
|
with Progress(
|
||||||
if options["correspondent"]:
|
TextColumn("[progress.description]{task.description}"),
|
||||||
set_correspondent(
|
BarColumn(),
|
||||||
sender=None,
|
TaskProgressColumn(),
|
||||||
document=document,
|
TimeRemainingColumn(),
|
||||||
classifier=classifier,
|
disable=self.no_progress_bar,
|
||||||
replace=options["overwrite"],
|
) as progress:
|
||||||
use_first=options["use_first"],
|
task = progress.add_task("Retagging documents", total=documents.count())
|
||||||
suggest=options["suggest"],
|
for document in documents:
|
||||||
base_url=options["base_url"],
|
if options["correspondent"]:
|
||||||
stdout=self.stdout,
|
set_correspondent(
|
||||||
style_func=self.style,
|
sender=None,
|
||||||
)
|
document=document,
|
||||||
|
classifier=classifier,
|
||||||
|
replace=options["overwrite"],
|
||||||
|
use_first=options["use_first"],
|
||||||
|
suggest=options["suggest"],
|
||||||
|
base_url=options["base_url"],
|
||||||
|
stdout=self.stdout,
|
||||||
|
style_func=self.style,
|
||||||
|
)
|
||||||
|
|
||||||
if options["document_type"]:
|
if options["document_type"]:
|
||||||
set_document_type(
|
set_document_type(
|
||||||
sender=None,
|
sender=None,
|
||||||
document=document,
|
document=document,
|
||||||
classifier=classifier,
|
classifier=classifier,
|
||||||
replace=options["overwrite"],
|
replace=options["overwrite"],
|
||||||
use_first=options["use_first"],
|
use_first=options["use_first"],
|
||||||
suggest=options["suggest"],
|
suggest=options["suggest"],
|
||||||
base_url=options["base_url"],
|
base_url=options["base_url"],
|
||||||
stdout=self.stdout,
|
stdout=self.stdout,
|
||||||
style_func=self.style,
|
style_func=self.style,
|
||||||
)
|
)
|
||||||
|
|
||||||
if options["tags"]:
|
if options["tags"]:
|
||||||
set_tags(
|
set_tags(
|
||||||
sender=None,
|
sender=None,
|
||||||
document=document,
|
document=document,
|
||||||
classifier=classifier,
|
classifier=classifier,
|
||||||
replace=options["overwrite"],
|
replace=options["overwrite"],
|
||||||
suggest=options["suggest"],
|
suggest=options["suggest"],
|
||||||
base_url=options["base_url"],
|
base_url=options["base_url"],
|
||||||
stdout=self.stdout,
|
stdout=self.stdout,
|
||||||
style_func=self.style,
|
style_func=self.style,
|
||||||
)
|
)
|
||||||
if options["storage_path"]:
|
if options["storage_path"]:
|
||||||
set_storage_path(
|
set_storage_path(
|
||||||
sender=None,
|
sender=None,
|
||||||
document=document,
|
document=document,
|
||||||
classifier=classifier,
|
classifier=classifier,
|
||||||
replace=options["overwrite"],
|
replace=options["overwrite"],
|
||||||
use_first=options["use_first"],
|
use_first=options["use_first"],
|
||||||
suggest=options["suggest"],
|
suggest=options["suggest"],
|
||||||
base_url=options["base_url"],
|
base_url=options["base_url"],
|
||||||
stdout=self.stdout,
|
stdout=self.stdout,
|
||||||
style_func=self.style,
|
style_func=self.style,
|
||||||
)
|
)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import logging
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from django import db
|
from django import db
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.management.commands.mixins import MultiProcessMixin
|
from documents.management.commands.mixins import MultiProcessMixin
|
||||||
from documents.management.commands.mixins import ProgressBarMixin
|
from documents.management.commands.mixins import ProgressBarMixin
|
||||||
@@ -70,15 +74,19 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
|
|||||||
# with postgres.
|
# with postgres.
|
||||||
db.connections.close_all()
|
db.connections.close_all()
|
||||||
|
|
||||||
if self.process_count == 1:
|
with Progress(
|
||||||
for doc_id in ids:
|
TextColumn("[progress.description]{task.description}"),
|
||||||
_process_document(doc_id)
|
BarColumn(),
|
||||||
else: # pragma: no cover
|
TaskProgressColumn(),
|
||||||
with multiprocessing.Pool(processes=self.process_count) as pool:
|
TimeRemainingColumn(),
|
||||||
list(
|
disable=self.no_progress_bar,
|
||||||
tqdm.tqdm(
|
) as progress:
|
||||||
pool.imap_unordered(_process_document, ids),
|
task = progress.add_task("Generating thumbnails", total=len(ids))
|
||||||
total=len(ids),
|
if self.process_count == 1:
|
||||||
disable=self.no_progress_bar,
|
for doc_id in ids:
|
||||||
),
|
_process_document(doc_id)
|
||||||
)
|
progress.update(task, advance=1)
|
||||||
|
else: # pragma: no cover
|
||||||
|
with multiprocessing.Pool(processes=self.process_count) as pool:
|
||||||
|
for _ in pool.imap_unordered(_process_document, ids):
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from tqdm import tqdm
|
from rich.console import Console
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.management.commands.mixins import ProgressBarMixin
|
from documents.management.commands.mixins import ProgressBarMixin
|
||||||
|
|
||||||
@@ -18,22 +23,37 @@ class Command(BaseCommand, ProgressBarMixin):
|
|||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
self.handle_progress_bar_mixin(**options)
|
self.handle_progress_bar_mixin(**options)
|
||||||
|
console = Console()
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for log_entry in tqdm(LogEntry.objects.all(), disable=self.no_progress_bar):
|
log_entries = LogEntry.objects.all()
|
||||||
model_class = log_entry.content_type.model_class()
|
with Progress(
|
||||||
# use global_objects for SoftDeleteModel
|
TextColumn("[progress.description]{task.description}"),
|
||||||
objects = (
|
BarColumn(),
|
||||||
model_class.global_objects
|
TaskProgressColumn(),
|
||||||
if hasattr(model_class, "global_objects")
|
TimeRemainingColumn(),
|
||||||
else model_class.objects
|
console=console,
|
||||||
|
disable=self.no_progress_bar,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task(
|
||||||
|
"Pruning audit logs",
|
||||||
|
total=log_entries.count(),
|
||||||
)
|
)
|
||||||
if (
|
for log_entry in log_entries:
|
||||||
log_entry.object_id
|
model_class = log_entry.content_type.model_class()
|
||||||
and not objects.filter(pk=log_entry.object_id).exists()
|
# use global_objects for SoftDeleteModel
|
||||||
):
|
objects = (
|
||||||
log_entry.delete()
|
model_class.global_objects
|
||||||
tqdm.write(
|
if hasattr(model_class, "global_objects")
|
||||||
self.style.NOTICE(
|
else model_class.objects
|
||||||
f"Deleted audit log entry for {model_class.__name__} #{log_entry.object_id}",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
log_entry.object_id
|
||||||
|
and not objects.filter(pk=log_entry.object_id).exists()
|
||||||
|
):
|
||||||
|
log_entry.delete()
|
||||||
|
console.print(
|
||||||
|
self.style.NOTICE(
|
||||||
|
f"Deleted audit log entry for {model_class.__name__} #{log_entry.object_id}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ from typing import Final
|
|||||||
from celery import states
|
from celery import states
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from tqdm import tqdm
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
@@ -92,76 +96,99 @@ def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages:
|
|||||||
if logo_file in present_files:
|
if logo_file in present_files:
|
||||||
present_files.remove(logo_file)
|
present_files.remove(logo_file)
|
||||||
|
|
||||||
for doc in tqdm(Document.global_objects.all(), disable=not progress):
|
documents = Document.global_objects.all()
|
||||||
# Check sanity of the thumbnail
|
with Progress(
|
||||||
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve()
|
TextColumn("[progress.description]{task.description}"),
|
||||||
if not thumbnail_path.exists() or not thumbnail_path.is_file():
|
BarColumn(),
|
||||||
messages.error(doc.pk, "Thumbnail of document does not exist.")
|
TaskProgressColumn(),
|
||||||
else:
|
TimeRemainingColumn(),
|
||||||
if thumbnail_path in present_files:
|
disable=not progress,
|
||||||
present_files.remove(thumbnail_path)
|
) as progress_bar:
|
||||||
try:
|
task = progress_bar.add_task(
|
||||||
_ = thumbnail_path.read_bytes()
|
"Checking document sanity",
|
||||||
except OSError as e:
|
total=documents.count(),
|
||||||
messages.error(doc.pk, f"Cannot read thumbnail file of document: {e}")
|
)
|
||||||
|
for doc in documents:
|
||||||
# Check sanity of the original file
|
# Check sanity of the thumbnail
|
||||||
# TODO: extract method
|
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve()
|
||||||
source_path: Final[Path] = Path(doc.source_path).resolve()
|
if not thumbnail_path.exists() or not thumbnail_path.is_file():
|
||||||
if not source_path.exists() or not source_path.is_file():
|
messages.error(doc.pk, "Thumbnail of document does not exist.")
|
||||||
messages.error(doc.pk, "Original of document does not exist.")
|
|
||||||
else:
|
|
||||||
if source_path in present_files:
|
|
||||||
present_files.remove(source_path)
|
|
||||||
try:
|
|
||||||
checksum = hashlib.md5(source_path.read_bytes()).hexdigest()
|
|
||||||
except OSError as e:
|
|
||||||
messages.error(doc.pk, f"Cannot read original file of document: {e}")
|
|
||||||
else:
|
else:
|
||||||
if checksum != doc.checksum:
|
if thumbnail_path in present_files:
|
||||||
messages.error(
|
present_files.remove(thumbnail_path)
|
||||||
doc.pk,
|
|
||||||
"Checksum mismatch. "
|
|
||||||
f"Stored: {doc.checksum}, actual: {checksum}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check sanity of the archive file.
|
|
||||||
if doc.archive_checksum is not None and doc.archive_filename is None:
|
|
||||||
messages.error(
|
|
||||||
doc.pk,
|
|
||||||
"Document has an archive file checksum, but no archive filename.",
|
|
||||||
)
|
|
||||||
elif doc.archive_checksum is None and doc.archive_filename is not None:
|
|
||||||
messages.error(
|
|
||||||
doc.pk,
|
|
||||||
"Document has an archive file, but its checksum is missing.",
|
|
||||||
)
|
|
||||||
elif doc.has_archive_version:
|
|
||||||
archive_path: Final[Path] = Path(doc.archive_path).resolve()
|
|
||||||
if not archive_path.exists() or not archive_path.is_file():
|
|
||||||
messages.error(doc.pk, "Archived version of document does not exist.")
|
|
||||||
else:
|
|
||||||
if archive_path in present_files:
|
|
||||||
present_files.remove(archive_path)
|
|
||||||
try:
|
try:
|
||||||
checksum = hashlib.md5(archive_path.read_bytes()).hexdigest()
|
_ = thumbnail_path.read_bytes()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
messages.error(
|
messages.error(
|
||||||
doc.pk,
|
doc.pk,
|
||||||
f"Cannot read archive file of document : {e}",
|
f"Cannot read thumbnail file of document: {e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check sanity of the original file
|
||||||
|
# TODO: extract method
|
||||||
|
source_path: Final[Path] = Path(doc.source_path).resolve()
|
||||||
|
if not source_path.exists() or not source_path.is_file():
|
||||||
|
messages.error(doc.pk, "Original of document does not exist.")
|
||||||
|
else:
|
||||||
|
if source_path in present_files:
|
||||||
|
present_files.remove(source_path)
|
||||||
|
try:
|
||||||
|
checksum = hashlib.md5(source_path.read_bytes()).hexdigest()
|
||||||
|
except OSError as e:
|
||||||
|
messages.error(
|
||||||
|
doc.pk,
|
||||||
|
f"Cannot read original file of document: {e}",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if checksum != doc.archive_checksum:
|
if checksum != doc.checksum:
|
||||||
messages.error(
|
messages.error(
|
||||||
doc.pk,
|
doc.pk,
|
||||||
"Checksum mismatch of archived document. "
|
"Checksum mismatch. "
|
||||||
f"Stored: {doc.archive_checksum}, "
|
f"Stored: {doc.checksum}, actual: {checksum}.",
|
||||||
f"actual: {checksum}.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# other document checks
|
# Check sanity of the archive file.
|
||||||
if not doc.content:
|
if doc.archive_checksum is not None and doc.archive_filename is None:
|
||||||
messages.info(doc.pk, "Document contains no OCR data")
|
messages.error(
|
||||||
|
doc.pk,
|
||||||
|
"Document has an archive file checksum, but no archive filename.",
|
||||||
|
)
|
||||||
|
elif doc.archive_checksum is None and doc.archive_filename is not None:
|
||||||
|
messages.error(
|
||||||
|
doc.pk,
|
||||||
|
"Document has an archive file, but its checksum is missing.",
|
||||||
|
)
|
||||||
|
elif doc.has_archive_version:
|
||||||
|
archive_path: Final[Path] = Path(doc.archive_path).resolve()
|
||||||
|
if not archive_path.exists() or not archive_path.is_file():
|
||||||
|
messages.error(
|
||||||
|
doc.pk,
|
||||||
|
"Archived version of document does not exist.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if archive_path in present_files:
|
||||||
|
present_files.remove(archive_path)
|
||||||
|
try:
|
||||||
|
checksum = hashlib.md5(archive_path.read_bytes()).hexdigest()
|
||||||
|
except OSError as e:
|
||||||
|
messages.error(
|
||||||
|
doc.pk,
|
||||||
|
f"Cannot read archive file of document : {e}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if checksum != doc.archive_checksum:
|
||||||
|
messages.error(
|
||||||
|
doc.pk,
|
||||||
|
"Checksum mismatch of archived document. "
|
||||||
|
f"Stored: {doc.archive_checksum}, "
|
||||||
|
f"actual: {checksum}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# other document checks
|
||||||
|
if not doc.content:
|
||||||
|
messages.info(doc.pk, "Document contains no OCR data")
|
||||||
|
|
||||||
|
progress_bar.update(task, advance=1)
|
||||||
|
|
||||||
for extra_file in present_files:
|
for extra_file in present_files:
|
||||||
messages.warning(None, f"Orphaned file in media dir: {extra_file}")
|
messages.warning(None, f"Orphaned file in media dir: {extra_file}")
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from pathlib import Path
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
|
|
||||||
import tqdm
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from celery import states
|
from celery import states
|
||||||
@@ -19,6 +18,11 @@ from django.db import transaction
|
|||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
from whoosh.writing import AsyncWriter
|
from whoosh.writing import AsyncWriter
|
||||||
|
|
||||||
from documents import index
|
from documents import index
|
||||||
@@ -83,9 +87,20 @@ def index_reindex(*, progress_bar_disable=False) -> None:
|
|||||||
|
|
||||||
ix = index.open_index(recreate=True)
|
ix = index.open_index(recreate=True)
|
||||||
|
|
||||||
with AsyncWriter(ix) as writer:
|
with (
|
||||||
for document in tqdm.tqdm(documents, disable=progress_bar_disable):
|
AsyncWriter(ix) as writer,
|
||||||
|
Progress(
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
disable=progress_bar_disable,
|
||||||
|
) as progress,
|
||||||
|
):
|
||||||
|
task = progress.add_task("Reindexing documents", total=documents.count())
|
||||||
|
for document in documents:
|
||||||
index.update_document(writer, document)
|
index.update_document(writer, document)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
import faiss
|
import faiss
|
||||||
import llama_index.core.settings as llama_settings
|
import llama_index.core.settings as llama_settings
|
||||||
import tqdm
|
|
||||||
from celery import states
|
from celery import states
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -22,6 +21,11 @@ from llama_index.core.storage.docstore import SimpleDocumentStore
|
|||||||
from llama_index.core.storage.index_store import SimpleIndexStore
|
from llama_index.core.storage.index_store import SimpleIndexStore
|
||||||
from llama_index.core.text_splitter import TokenTextSplitter
|
from llama_index.core.text_splitter import TokenTextSplitter
|
||||||
from llama_index.vector_stores.faiss import FaissVectorStore
|
from llama_index.vector_stores.faiss import FaissVectorStore
|
||||||
|
from rich.progress import BarColumn
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.progress import TaskProgressColumn
|
||||||
|
from rich.progress import TextColumn
|
||||||
|
from rich.progress import TimeRemainingColumn
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
@@ -176,9 +180,18 @@ def update_llm_index(*, progress_bar_disable=False, rebuild=False) -> str:
|
|||||||
embed_model = get_embedding_model()
|
embed_model = get_embedding_model()
|
||||||
llama_settings.Settings.embed_model = embed_model
|
llama_settings.Settings.embed_model = embed_model
|
||||||
storage_context = get_or_create_storage_context(rebuild=True)
|
storage_context = get_or_create_storage_context(rebuild=True)
|
||||||
for document in tqdm.tqdm(documents, disable=progress_bar_disable):
|
with Progress(
|
||||||
document_nodes = build_document_node(document)
|
TextColumn("[progress.description]{task.description}"),
|
||||||
nodes.extend(document_nodes)
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
disable=progress_bar_disable,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task("Building document nodes", total=documents.count())
|
||||||
|
for document in documents:
|
||||||
|
document_nodes = build_document_node(document)
|
||||||
|
nodes.extend(document_nodes)
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|
||||||
index = VectorStoreIndex(
|
index = VectorStoreIndex(
|
||||||
nodes=nodes,
|
nodes=nodes,
|
||||||
@@ -196,23 +209,33 @@ def update_llm_index(*, progress_bar_disable=False, rebuild=False) -> str:
|
|||||||
for node in index.docstore.get_nodes(all_node_ids)
|
for node in index.docstore.get_nodes(all_node_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
for document in tqdm.tqdm(documents, disable=progress_bar_disable):
|
with Progress(
|
||||||
doc_id = str(document.id)
|
TextColumn("[progress.description]{task.description}"),
|
||||||
document_modified = document.modified.isoformat()
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
disable=progress_bar_disable,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task("Updating index nodes", total=documents.count())
|
||||||
|
for document in documents:
|
||||||
|
doc_id = str(document.id)
|
||||||
|
document_modified = document.modified.isoformat()
|
||||||
|
|
||||||
if doc_id in existing_nodes:
|
if doc_id in existing_nodes:
|
||||||
node = existing_nodes[doc_id]
|
node = existing_nodes[doc_id]
|
||||||
node_modified = node.metadata.get("modified")
|
node_modified = node.metadata.get("modified")
|
||||||
|
|
||||||
if node_modified == document_modified:
|
if node_modified == document_modified:
|
||||||
continue
|
progress.update(task, advance=1)
|
||||||
|
continue
|
||||||
|
|
||||||
# Again, delete from docstore, FAISS IndexFlatL2 are append-only
|
# Again, delete from docstore, FAISS IndexFlatL2 are append-only
|
||||||
index.docstore.delete_document(node.node_id)
|
index.docstore.delete_document(node.node_id)
|
||||||
nodes.extend(build_document_node(document))
|
nodes.extend(build_document_node(document))
|
||||||
else:
|
else:
|
||||||
# New document, add it
|
# New document, add it
|
||||||
nodes.extend(build_document_node(document))
|
nodes.extend(build_document_node(document))
|
||||||
|
progress.update(task, advance=1)
|
||||||
|
|
||||||
if nodes:
|
if nodes:
|
||||||
msg = "LLM index updated successfully."
|
msg = "LLM index updated successfully."
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ def test_update_llm_index(
|
|||||||
mock_queryset = MagicMock()
|
mock_queryset = MagicMock()
|
||||||
mock_queryset.exists.return_value = True
|
mock_queryset.exists.return_value = True
|
||||||
mock_queryset.__iter__.return_value = iter([real_document])
|
mock_queryset.__iter__.return_value = iter([real_document])
|
||||||
|
mock_queryset.count.return_value = 1
|
||||||
mock_all.return_value = mock_queryset
|
mock_all.return_value = mock_queryset
|
||||||
indexing.update_llm_index(rebuild=True)
|
indexing.update_llm_index(rebuild=True)
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ def test_update_llm_index_removes_meta(
|
|||||||
mock_queryset = MagicMock()
|
mock_queryset = MagicMock()
|
||||||
mock_queryset.exists.return_value = True
|
mock_queryset.exists.return_value = True
|
||||||
mock_queryset.__iter__.return_value = iter([real_document])
|
mock_queryset.__iter__.return_value = iter([real_document])
|
||||||
|
mock_queryset.count.return_value = 1
|
||||||
mock_all.return_value = mock_queryset
|
mock_all.return_value = mock_queryset
|
||||||
indexing.update_llm_index(rebuild=True)
|
indexing.update_llm_index(rebuild=True)
|
||||||
|
|
||||||
@@ -129,6 +131,7 @@ def test_update_llm_index_partial_update(
|
|||||||
mock_queryset = MagicMock()
|
mock_queryset = MagicMock()
|
||||||
mock_queryset.exists.return_value = True
|
mock_queryset.exists.return_value = True
|
||||||
mock_queryset.__iter__.return_value = iter([real_document, doc2])
|
mock_queryset.__iter__.return_value = iter([real_document, doc2])
|
||||||
|
mock_queryset.count.return_value = 2
|
||||||
mock_all.return_value = mock_queryset
|
mock_all.return_value = mock_queryset
|
||||||
|
|
||||||
indexing.update_llm_index(rebuild=True)
|
indexing.update_llm_index(rebuild=True)
|
||||||
@@ -149,6 +152,7 @@ def test_update_llm_index_partial_update(
|
|||||||
mock_queryset = MagicMock()
|
mock_queryset = MagicMock()
|
||||||
mock_queryset.exists.return_value = True
|
mock_queryset.exists.return_value = True
|
||||||
mock_queryset.__iter__.return_value = iter([updated_document, doc2, doc3])
|
mock_queryset.__iter__.return_value = iter([updated_document, doc2, doc3])
|
||||||
|
mock_queryset.count.return_value = 3
|
||||||
mock_all.return_value = mock_queryset
|
mock_all.return_value = mock_queryset
|
||||||
|
|
||||||
# assert logs "Updating LLM index with %d new nodes and removing %d old nodes."
|
# assert logs "Updating LLM index with %d new nodes and removing %d old nodes."
|
||||||
|
|||||||
241
uv.lock
generated
241
uv.lock
generated
@@ -322,20 +322,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "backrefs"
|
|
||||||
version = "6.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "banks"
|
name = "banks"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -944,7 +930,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deprecated"
|
name = "deprecated"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1507,18 +1492,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ghp-import"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gotenberg-client"
|
name = "gotenberg-client"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@@ -2302,15 +2275,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "llama-index-llms-openai"
|
name = "llama-index-llms-openai"
|
||||||
version = "0.6.17"
|
version = "0.6.18"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "llama-index-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/94/55e9855f565b30604fe9e154a7c3df1f4437ce62ed8926a8d6b5112cf73b/llama_index_llms_openai-0.6.17.tar.gz", hash = "sha256:63294c1d8d221d2009023b2c294c919f6141f4cdce9cd48bffe700d8be2a37e0", size = 25941, upload-time = "2026-02-03T11:03:26.355Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/56/78/298de76242aee7f5fdd65a0bffb541b3f81759613de1e8ebc719eec8e8af/llama_index_llms_openai-0.6.18.tar.gz", hash = "sha256:36c0256a7a211bbbc5ecc00d3f2caa9730eea1971ced3b68b7c94025c0448020", size = 25946, upload-time = "2026-02-06T12:01:03.095Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/85/1dee39a45b31528611cc6139777fc5631b9946fe085b37cd981062aa6d50/llama_index_llms_openai-0.6.17-py3-none-any.whl", hash = "sha256:d275f7e218611afc6bb157fe722f6e8590e8e20ac3ea1392cf560ac674c175af", size = 26940, upload-time = "2026-02-03T11:03:25.059Z" },
|
{ url = "https://files.pythonhosted.org/packages/39/46/5a4b62108fb94febe27d35c8476dea042d7a609ee4bf14f5b61f03d5a75a/llama_index_llms_openai-0.6.18-py3-none-any.whl", hash = "sha256:73bbbf233d38116d48350391a3649884829564f4c8f6168c8fa3f3ae1b557376", size = 26945, upload-time = "2026-02-06T12:01:01.25Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2558,15 +2531,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mergedeep"
|
|
||||||
version = "1.3.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "microsoft-python-type-stubs"
|
name = "microsoft-python-type-stubs"
|
||||||
version = "0"
|
version = "0"
|
||||||
@@ -3054,15 +3018,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paginate"
|
|
||||||
version = "0.5.7"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.20.6"
|
version = "2.20.6"
|
||||||
@@ -3128,7 +3083,6 @@ dependencies = [
|
|||||||
{ name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
{ name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
|
||||||
{ name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
|
{ name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" },
|
||||||
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -3211,7 +3165,6 @@ typing = [
|
|||||||
{ name = "types-pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "types-pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "types-redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "types-redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "types-setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "types-setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "types-tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -3282,7 +3235,6 @@ requires-dist = [
|
|||||||
{ name = "setproctitle", specifier = "~=1.3.4" },
|
{ name = "setproctitle", specifier = "~=1.3.4" },
|
||||||
{ name = "tika-client", specifier = "~=0.10.0" },
|
{ name = "tika-client", specifier = "~=0.10.0" },
|
||||||
{ name = "torch", specifier = "~=2.10.0", index = "https://download.pytorch.org/whl/cpu" },
|
{ name = "torch", specifier = "~=2.10.0", index = "https://download.pytorch.org/whl/cpu" },
|
||||||
{ name = "tqdm", specifier = "~=4.67.1" },
|
|
||||||
{ name = "watchfiles", specifier = ">=1.1.1" },
|
{ name = "watchfiles", specifier = ">=1.1.1" },
|
||||||
{ name = "whitenoise", specifier = "~=6.11" },
|
{ name = "whitenoise", specifier = "~=6.11" },
|
||||||
{ name = "whoosh-reloaded", specifier = ">=2.7.5" },
|
{ name = "whoosh-reloaded", specifier = ">=2.7.5" },
|
||||||
@@ -3310,9 +3262,7 @@ dev = [
|
|||||||
{ name = "ruff", specifier = "~=0.15.0" },
|
{ name = "ruff", specifier = "~=0.15.0" },
|
||||||
{ name = "zensical", specifier = ">=0.0.21" },
|
{ name = "zensical", specifier = ">=0.0.21" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [{ name = "zensical", specifier = ">=0.0.21" }]
|
||||||
{ name = "zensical", specifier = ">=0.0.21" },
|
|
||||||
]
|
|
||||||
lint = [
|
lint = [
|
||||||
{ name = "prek", specifier = "~=0.3.0" },
|
{ name = "prek", specifier = "~=0.3.0" },
|
||||||
{ name = "ruff", specifier = "~=0.15.0" },
|
{ name = "ruff", specifier = "~=0.15.0" },
|
||||||
@@ -3351,7 +3301,6 @@ typing = [
|
|||||||
{ name = "types-pytz" },
|
{ name = "types-pytz" },
|
||||||
{ name = "types-redis" },
|
{ name = "types-redis" },
|
||||||
{ name = "types-setuptools" },
|
{ name = "types-setuptools" },
|
||||||
{ name = "types-tqdm" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3591,23 +3540,23 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prek"
|
name = "prek"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/76/62/4b91c8a343e21fcefabc569a91d08cf8756c554332521af78294acef7c27/prek-0.3.1.tar.gz", hash = "sha256:45abc4ffd3cb2d39c478f47e92e88f050e5a4b7a20915d78e54b1a3d3619ebe4", size = 323141, upload-time = "2026-01-31T13:25:58.128Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/f5/ee52def928dd1355c20bcfcf765e1e61434635c33f3075e848e7b83a157b/prek-0.3.2.tar.gz", hash = "sha256:dce0074ff1a21290748ca567b4bda7553ee305a8c7b14d737e6c58364a499364", size = 334229, upload-time = "2026-02-06T13:49:47.539Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/c1/e0e545048e4190245fb4ae375d67684108518f3c69b67b81d62e1cd855c6/prek-0.3.1-py3-none-linux_armv6l.whl", hash = "sha256:1f77d0845cc63cad9c447f7f7d554c1ad188d07dbe02741823d20d58c7312eaf", size = 4285460, upload-time = "2026-01-31T13:25:42.066Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/69/70a5fc881290a63910494df2677c0fb241d27cfaa435bbcd0de5cd2e2443/prek-0.3.2-py3-none-linux_armv6l.whl", hash = "sha256:4f352f9c3fc98aeed4c8b2ec4dbf16fc386e45eea163c44d67e5571489bd8e6f", size = 4614960, upload-time = "2026-02-06T13:50:05.818Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/fe/7636d10e2dafdf2a4a927c989f32ce3f08e99d62ebad7563f0272e74b7f4/prek-0.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e21142300d139e8c7f3970dd8aa66391cb43cd17c0c4ee65ff1af48856bb6a4b", size = 4287085, upload-time = "2026-01-31T13:25:40.193Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/15/a82d5d32a2207ccae5d86ea9e44f2b93531ed000faf83a253e8d1108e026/prek-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4a000cfbc3a6ec7d424f8be3c3e69ccd595448197f92daac8652382d0acc2593", size = 4622889, upload-time = "2026-02-06T13:49:53.662Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/7f/62ed57340071e04c02057d64276ec3986baca3ad4759523e2f433dc9be55/prek-0.3.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c09391de7d1994f9402c46cb31671800ea309b1668d839614965706206da3655", size = 3936188, upload-time = "2026-01-31T13:25:47.314Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/75/ea833b58a12741397017baef9b66a6e443bfa8286ecbd645d14111446280/prek-0.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5436bdc2702cbd7bcf9e355564ae66f8131211e65fefae54665a94a07c3d450a", size = 4239653, upload-time = "2026-02-06T13:50:02.88Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/17/cb24f462c255f76d130ca110f4fcec09b041c3c770e43960cc3fc9dcc9ce/prek-0.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a0b0a012ef6ef28dee019cf81a95c5759b2c03287c32c1f2adcb5f5fb097138e", size = 4275214, upload-time = "2026-01-31T13:25:38.053Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/b4/d9c3885987afac6e20df4cb7db14e3b0d5a08a77ae4916488254ebac4d0b/prek-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0161b5f584f9e7f416d6cf40a17b98f17953050ff8d8350ec60f20fe966b86b6", size = 4595101, upload-time = "2026-02-06T13:49:49.813Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/85/db155b59d73cf972c8467e4d95def635f9976d5fcbcc790a4bbe9d2e049a/prek-0.3.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4a0e40785d39b8feae0d7ecf5534789811a2614114ab47f4e344a2ebd75ac10", size = 4197982, upload-time = "2026-01-31T13:25:50.034Z" },
|
{ url = "https://files.pythonhosted.org/packages/21/a6/1a06473ed83dbc898de22838abdb13954e2583ce229f857f61828384634c/prek-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e641e8533bca38797eebb49aa89ed0e8db0e61225943b27008c257e3af4d631", size = 4521978, upload-time = "2026-02-06T13:49:41.266Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/cf/d35c32436692928a9ca53eed3334e30148a60f0faa33c42e8d11b6028fa6/prek-0.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5c2f5e377e3cc5a5ea191deb8255a5823fbaa01b424417fe18ff12c7c800f3", size = 4458572, upload-time = "2026-01-31T13:25:51.46Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/5e/c38390d5612e6d86b32151c1d2fdab74a57913473193591f0eb00c894c21/prek-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfca1810d49d3f9ef37599c958c4e716bc19a1d78a7e88cbdcb332e0b008994f", size = 4829108, upload-time = "2026-02-06T13:49:44.598Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/c0/eb36fecb21fe30baa72fb87ccf3a791c32932786c287f95f8972402e9245/prek-0.3.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fe70e97f4dfca57ce142caecd77b90a435abd1c855f9e96041078415d80e89a", size = 4999230, upload-time = "2026-01-31T13:25:44.055Z" },
|
{ url = "https://files.pythonhosted.org/packages/80/a6/cecce2ab623747ff65ed990bb0d95fa38449ee19b348234862acf9392fff/prek-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d69d754299a95a85dc20196f633232f306bee7e7c8cba61791f49ce70404ec", size = 5357520, upload-time = "2026-02-06T13:49:48.512Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/f3/ad1a25ea16320e6acd1fedd6bd96a0d22526f5132d9b5adc045996ccca4c/prek-0.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b28e921d893771bdd7533cd94d46a10e0cf2855c5e6bf6809b598b5e45baa73", size = 4510376, upload-time = "2026-01-31T13:25:48.563Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/18/d6bcb29501514023c76d55d5cd03bdbc037737c8de8b6bc41cdebfb1682c/prek-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:539dcb90ad9b20837968539855df6a29493b328a1ae87641560768eed4f313b0", size = 4852635, upload-time = "2026-02-06T13:49:58.347Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/b7/91afdd24be808ccf3b9119f4cf2bd6d02e30683143a62a08f432a3435861/prek-0.3.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:555610a53c4541976f4fe8602177ecd7a86a931dcb90a89e5826cfc4a6c8f2cb", size = 4273229, upload-time = "2026-01-31T13:25:56.362Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/0a/ae46f34ba27ba87aea5c9ad4ac9cd3e07e014fd5079ae079c84198f62118/prek-0.3.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1998db3d0cbe243984736c82232be51318f9192e2433919a6b1c5790f600b5fd", size = 4599484, upload-time = "2026-02-06T13:49:43.296Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/bb/636c77c5c9fc5eadcc2af975a95b48eeeff2dc833021e222b0e9479b9b47/prek-0.3.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:663a15a31b705db5d01a1c9eb28c6ea417628e6cb68de2dc7b3016ca2f959a99", size = 4301166, upload-time = "2026-01-31T13:25:36.281Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/a9/73bfb5b3f7c3583f9b0d431924873928705cdef6abb3d0461c37254a681b/prek-0.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:07ab237a5415a3e8c0db54de9d63899bcd947624bdd8820d26f12e65f8d19eb7", size = 4657694, upload-time = "2026-02-06T13:50:01.074Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/cf/c928a36173e71b21b252943404a5b3d1ddc1f08c9e0f1d7282a2c62c7325/prek-0.3.1-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1d03fb5fa37177dc37ccbe474252473adcbde63287a63e9fa3d5745470f95bd8", size = 4188473, upload-time = "2026-01-31T13:25:53.341Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/bc/0994bc176e1a80110fad3babce2c98b0ac4007630774c9e18fc200a34781/prek-0.3.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0ced19701d69c14a08125f14a5dd03945982edf59e793c73a95caf4697a7ac30", size = 4509337, upload-time = "2026-02-06T13:49:54.891Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/4c/af8f6a40cb094e88225514b89e8ae05ac69fc479d6b500e4b984f9ef8ae3/prek-0.3.1-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3e20a5b704c06944dca9555a39f1de3622edc8ed2becaf8b3564925d1b7c1c0d", size = 4342239, upload-time = "2026-01-31T13:25:55.179Z" },
|
{ url = "https://files.pythonhosted.org/packages/f9/13/e73f85f65ba8f626468e5d1694ab3763111513da08e0074517f40238c061/prek-0.3.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ffb28189f976fa111e770ee94e4f298add307714568fb7d610c8a7095cb1ce59", size = 4697350, upload-time = "2026-02-06T13:50:04.526Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/ba/6b0f725c0cf96182ab9622b4d122a01f04de9b2b6e4a6516874390218510/prek-0.3.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6889acf0c9b0dd7b9cd3510ec36af10a739826d2b9de1e2d021ae6a9f130959a", size = 4618674, upload-time = "2026-01-31T13:25:59.175Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/47/98c46dcd580305b9960252a4eb966f1a7b1035c55c363f378d85662ba400/prek-0.3.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f63134b3eea14421789a7335d86f99aee277cb520427196f2923b9260c60e5c5", size = 4955860, upload-time = "2026-02-06T13:49:56.581Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4330,18 +4279,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyyaml-env-tag"
|
|
||||||
version = "1.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyzbar"
|
name = "pyzbar"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -4898,50 +4835,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "selectolax"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/c5/959b8661d200d9fd3cf52061ce6f1d7087ec94823bb324fe1ca76c80b250/selectolax-0.4.6.tar.gz", hash = "sha256:bd9326cfc9bbd5bfcda980b0452b9761b3cf134015154e95d83fa32cbfbb751b", size = 4793248, upload-time = "2025-12-06T12:35:48.513Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/b6/a4753cdde612caf292abb3519b8770a004cb696fac0aea40b403f5e27672/selectolax-0.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e870cc935a36b8e37945c5e2055191df84e7af278dd386e5ff9f04552e5da6b", size = 2052715, upload-time = "2025-12-06T12:34:08.527Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/03/a3b21e54fbe5fee47d5e27fdb5696467c7d9adb7308d1bc85f1468a4d08e/selectolax-0.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9008e4c5f708634baa6dbeb4cd8922edc8467eb842cf12da5cdf03399a969f3", size = 2051040, upload-time = "2025-12-06T12:34:10.365Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/ad/3984fa06ece9c773acc7938ccbee922d437d5396fb331f22a122da6fea8d/selectolax-0.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c7120afee58104f9d708b62b4e14caa9dc88c23e3f752b62a43dac953963362", size = 2237979, upload-time = "2025-12-06T12:34:12.053Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/f2/ed47a855ca70db65b1c5d90e6cbb63132687a0b623a2edca5de26a68cadc/selectolax-0.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ceb1af4892aafd26612b1974c49abf524b5f69b2a9d521381d97dc8ca5c0e3d7", size = 2274003, upload-time = "2025-12-06T12:34:13.807Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/74/840b0cd0a178718c815610fbe704803b4cf98aa1a8eb218788b77c020811/selectolax-0.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a7a28857b7bd72e904c3e2fce9a21ceca60093622b3ceaf8e231a9ec24a0ca1", size = 2251372, upload-time = "2025-12-06T12:34:15.611Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/56/07bdcadb0180541b36c3f5dccd1d5243503ab695c2df6030e533ea098529/selectolax-0.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59162e0b78d82c8b60e1318b3e72b49eed5858c2640f1df759d3f60584e4f215", size = 2280044, upload-time = "2025-12-06T12:34:17.382Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/76/9b5358d82353b68c5828cc8ceae4ff1073495462979d2035c1089f4421dd/selectolax-0.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:751f727c9963584fcfa101b2696e0dd31236142901bbe7866a8e39f2ec346cc4", size = 2052057, upload-time = "2025-12-06T12:34:24.448Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/d1/9f8c8841f414a1b72174acef916d4ed38cc0fa041d3e933013e2b3213f30/selectolax-0.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6f45737b638836b9fe2a4e7ccfbcd48a315ebb96da76a79a04b8227c1a967ee", size = 2050379, upload-time = "2025-12-06T12:34:26.383Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/c4/8e5ab9275a185a31d06b5ea58e3ba61d57e57700964cbd56fe074dbe458c/selectolax-0.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15a22c5cd9c23f09227b2b9c227d986396125a9b0eb21be655f22fe4ccc5679a", size = 2238005, upload-time = "2025-12-06T12:34:28.2Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/af/f4299d931a8e11ba1998cac70d407c5338436978325232eb252ac7f5aba2/selectolax-0.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64f771807f1a7353f4d6878c303bfbd6c6fe58897e301aa6d0de7e5e60cef61", size = 2272927, upload-time = "2025-12-06T12:34:29.955Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/5e/9afbb0e8495846bf97fa7725a6f97622ad85efc654cb3cbf4c881bd345de/selectolax-0.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0144a37fbf01695ccf2d0d24caaa058a28449cecb2c754bb9e616f339468d74", size = 2250901, upload-time = "2025-12-06T12:34:31.442Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/6b/914cc9c977fd21949af5776d25b9c011b6e72fb38569161ab6ca83d6130a/selectolax-0.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c9bdd4a9b3f71f28a0ee558a105451debd33cbe1ed350ebba7ad6b68d62815c", size = 2279842, upload-time = "2025-12-06T12:34:32.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/81/1fdf6633df840afd9d7054a3441f04cfb1fc9d098c6c9f3bd46a64fb632e/selectolax-0.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20615062d6062b197b61fd646e667591e987be3a894e8a8408d2a482ccddc747", size = 2051021, upload-time = "2025-12-06T12:34:38.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/54/d59738d090cb4df3a3a6297b7ec216b86d3ba7f320013c4bc8d4841c9f5d/selectolax-0.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c436006e2af6ade80b96411cf46652c11ced4f230032e25e1f5210b7522a4fe3", size = 2047409, upload-time = "2025-12-06T12:34:39.875Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/67/3b163ec18a128df3a3b59ce676a2dacfb26e714da81ba8a98e184b4ef187/selectolax-0.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:705a70b6f4e553e8c5299881269d3735a7df8a23711927a33caa16b4eaef580f", size = 2237052, upload-time = "2025-12-06T12:34:41.24Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/04/c3ae4a77e8cfa647b9177e727a7e80f64b160b65ad0db0dcb3738a4ef4a0/selectolax-0.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c04fd180689ed9450ad2453a3cba74cff2475a4281f76db9e18a658b7823e594", size = 2275615, upload-time = "2025-12-06T12:34:43.114Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/de/aaa016c44e63a1efd5525f6da6eac807388a06c70671091c735d93f13b74/selectolax-0.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb33eb0809e70ba4a475105d164c3f90a4bb711744ca69e20037298256b8e9d7", size = 2249186, upload-time = "2025-12-06T12:34:44.84Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/9a/a9cf9f0158b0804c7ea404d790af031830eb6452a4948853f7582eea6c51/selectolax-0.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:97f30b7c731f9f3328e9c6aef7ca3c17fbcbc4495e286a2cdad9a77bcadfadf1", size = 2282041, upload-time = "2025-12-06T12:34:46.19Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/87/7ed053cce7de8b29746c602851c67654287e25ec404d575911c6f40b671f/selectolax-0.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8303af0eeef6ab2f06f2b18e3c825df4f6984405a862c8e9e654621c26558aca", size = 2050412, upload-time = "2025-12-06T12:34:53.086Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/74/e8cd9b9b53da0e849b27a2ef68580915321ee52a662f015275a1cf6cca85/selectolax-0.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8b1fb5507d90e719de705d93eaa3bdd5f2d69facf012fb67b7da8a9cd20ea6b", size = 2046513, upload-time = "2025-12-06T12:34:54.583Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/ca/c1cc7f03b681d272dbb0dc47cf9d0df857ae156d7ea54d88cc25ec23c8e9/selectolax-0.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ac51bd5bfcd6bffe58720b1fc5f97666681f0793d79d292069b3a3f8599ef0", size = 2234404, upload-time = "2025-12-06T12:34:55.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/00/a6e5c4d65243147fbd8837951267f098d9bee66ada4dc0c99d97259e052c/selectolax-0.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfee4d1269fd462256641abdf6c2ee4dd799ba82c578acab0bcde07547637826", size = 2272678, upload-time = "2025-12-06T12:34:57.3Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/a2/05351e0f0da62d1bc01b7820a990f1e4dec688206e0278ee8faeeb878940/selectolax-0.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fce09eeb5dd19fba672a829f63e3c40238af4a929ce1e5fd16dcbc4fd253e300", size = 2247471, upload-time = "2025-12-06T12:34:59.048Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/95/1ab9e3ad2d53dbcd7141af149c7259c693dc2dc46e27d72b7a68f17f3364/selectolax-0.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4e97ca71d545cab9c57ba3b18358cbc96ef0dcd01920ea903a531386ca1f849", size = 2278271, upload-time = "2025-12-06T12:35:00.487Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/38/93b4907c596487e13f8261129b1663559c96fc37ea2c973c98a393307484/selectolax-0.4.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:961c037ac41a12bf9db35a95d6076461f9b127d361c9ef26a77f76c45b1056eb", size = 2069131, upload-time = "2025-12-06T12:35:06.737Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/04/d29769a8313ccb9db6278401840e79662f341b716d268f7468b6270d15e1/selectolax-0.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2be076a69a12657a38933b99d31c604d95d63911e0799f89305da8e89918874", size = 2066134, upload-time = "2025-12-06T12:35:08.163Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/a2/1c26b4cc6a708d234e39199bd75acdc1cfdcf4f3138e16d25e3e7aa0295d/selectolax-0.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25ae5dd7c20035312211de43952934981e8ff4e1502467ce78665f57bc5eaf7f", size = 2240810, upload-time = "2025-12-06T12:35:09.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/de/b021342d306bd08c92d0d9e9072a3ed6a3038f78387187d37fb775196fa0/selectolax-0.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe1274930affe2b986fd20cbf2916ddf4a760edf30a7eeb9151b31e8cbe6027", size = 2274401, upload-time = "2025-12-06T12:35:11.023Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/77/b2816be2f4878f4e86fabca5932610401dc88d956948d97a82947e27d8bc/selectolax-0.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e52174a75236c1321f0f0353b5d97ba575c61428f16c2d78159cb154fa407e97", size = 2271054, upload-time = "2025-12-06T12:35:12.878Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/3a/ae48b9febf2af21a0fdd4fba07861ae3e13bd7235df3fa39918d33004058/selectolax-0.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5032187ed8f8d77da6dee5467a87fe9b1553c10c63671fe830e87a2d347ef8ae", size = 2297773, upload-time = "2025-12-06T12:35:14.383Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/ce/f3fa904517745ecd289b6ce18845ae968cf7d0b17e65ab781259f2746254/selectolax-0.4.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:48a9bce389c281fec8bf3b5a36bd376f1ad1f36ff072dcedaa977188b3882be1", size = 2088107, upload-time = "2025-12-06T12:35:21.061Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/18/359da9723d3ec1235819cea0958bc417ce6a12699977920854b300ad4529/selectolax-0.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff1632c1767f6005adc2dff7b72ea9d0b6493702e86e04ee5cf934ab630172aa", size = 2090836, upload-time = "2025-12-06T12:35:22.421Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/20/199f2a05ca8dfd9bc146af03fbfaa1e2050601d3141267070a2a023ea68f/selectolax-0.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64a56dd91c22e809305e127bbd4cd75ad1016dd329a4e945a176c6f3376d00e2", size = 2248608, upload-time = "2025-12-06T12:35:23.946Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/67/261c06cdae29fad287008d39f51a80431f3fce66c2865b880e61d04cdfd2/selectolax-0.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:223471d2c9095406d69faf461aa217782575d62362d36809e350f6d3c2f69f4e", size = 2275653, upload-time = "2025-12-06T12:35:25.423Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/80/f2519487a86b5366acadd9e07c212b38fb657bb62b9e01de9fb24c3ada27/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c1043e61df29f4f8ef49f9f623b3d710f0c545d9a7939eee52c49987b06aef7", size = 2283495, upload-time = "2025-12-06T12:35:26.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/cc/35a0fd896371e0b5a84365b03f88c7dbe8984862e34ce32baed81ee6f486/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b323ad4ebcf2edad2488022402fbc73ee158ffe81607ec1ce5eb1039eab94086", size = 2300207, upload-time = "2025-12-06T12:35:28.23Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentence-transformers"
|
name = "sentence-transformers"
|
||||||
version = "5.2.2"
|
version = "5.2.2"
|
||||||
@@ -5687,18 +5580,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/2b/7f/016dc5cc718ec6ccaa84fb73ed409ef1c261793fd5e637cdfaa18beb40a9/types_setuptools-80.10.0.20260124-py3-none-any.whl", hash = "sha256:efed7e044f01adb9c2806c7a8e1b6aa3656b8e382379b53d5f26ee3db24d4c01", size = 64333, upload-time = "2026-01-24T03:18:38.344Z" },
|
{ url = "https://files.pythonhosted.org/packages/2b/7f/016dc5cc718ec6ccaa84fb73ed409ef1c261793fd5e637cdfaa18beb40a9/types_setuptools-80.10.0.20260124-py3-none-any.whl", hash = "sha256:efed7e044f01adb9c2806c7a8e1b6aa3656b8e382379b53d5f26ee3db24d4c01", size = 64333, upload-time = "2026-01-24T03:18:38.344Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "types-tqdm"
|
|
||||||
version = "4.67.2.20260202"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/53/6a/a9e27944f2715940cf7b43b35a2b464d968d086723414983e47af3261e2d/types_tqdm-4.67.2.20260202.tar.gz", hash = "sha256:c3c494ae8dfd2946fa1fe089ce6931a85bb88d9078da0d65c6b99b6643f7561d", size = 17418, upload-time = "2026-02-02T04:11:26.288Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/f4/d24e5216630081356fd9427cb9fb4c94e3c6cfde4d4d46db2c6717e286ae/types_tqdm-4.67.2.20260202-py3-none-any.whl", hash = "sha256:01b31780b2a2cdad8465a3e53a06fdf53a47abab0d46d69c1b2039197ffd55b0", size = 23893, upload-time = "2026-02-02T04:11:25.007Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-webencodings"
|
name = "types-webencodings"
|
||||||
version = "0.5.0.20251108"
|
version = "0.5.0.20251108"
|
||||||
@@ -5901,35 +5782,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "watchdog"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchfiles"
|
name = "watchfiles"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -6235,6 +6087,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
|
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zensical"
|
||||||
|
version = "0.0.21"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "deepmerge", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/50/2655b5f72d0c72f4366be580f5e2354ff05280d047ea986fe89570e44589/zensical-0.0.21.tar.gz", hash = "sha256:c13563836fa63a3cabeffd83fe3a770ca740cfa5ae7b85df85d89837e31b3b4a", size = 3819731, upload-time = "2026-02-04T17:47:59.396Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/98/90710d232cb35b633815fa7b493da542391b89283b6103a5bb4ae9fc0dd9/zensical-0.0.21-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:67404cc70c330246dfb7269bcdb60a25be0bb60a212a09c9c50229a1341b1f84", size = 12237120, upload-time = "2026-02-04T17:47:28.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/fb/4280b3781157e8f051711732192f949bf29beeafd0df3e33c1c8bf9b7a1a/zensical-0.0.21-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4fd253ccfbf5af56434124f13bac01344e456c020148369b18d8836b6537c3c", size = 12118047, upload-time = "2026-02-04T17:47:31.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/b3/b7f85ae9cf920cf9f17bf157ae6c274919477148feb7716bf735636caa0e/zensical-0.0.21-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:440e40cdc30a29bf7466bcd6f43ed7bd1c54ea3f1a0fefca65619358b481a5bc", size = 12473440, upload-time = "2026-02-04T17:47:33.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/ac/1dc6e98f79ed19b9f103c88a0bd271f9140565d7d26b64bc1542b3ef6d91/zensical-0.0.21-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:368e832fc8068e75dc45cab59379db4cefcd81eb116f48d058db8fb7b7aa8d14", size = 12412588, upload-time = "2026-02-04T17:47:36.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/76/16a580f6dd32b387caa4a41615451e7dddd1917a2ff2e5b08744f41b4e11/zensical-0.0.21-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ab962d47f9dd73510eed168469326c7a452554dfbfdb9cdf85efc7140244df", size = 12749438, upload-time = "2026-02-04T17:47:38.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/30/4baaa1c910eee61db5f49d0d45f2e550a0027218c618f3dd7f8da966a019/zensical-0.0.21-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b846d53dfce007f056ff31848f87f3f2a388228e24d4851c0cafdce0fa204c9b", size = 12514504, upload-time = "2026-02-04T17:47:41.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/77/931fccae5580b94409a0448a26106f922dcfa7822e7b93cacd2876dd63a8/zensical-0.0.21-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:daac1075552d230d52d621d2e4754ba24d5afcaa201a7a991f1a8d57e320c9de", size = 12647832, upload-time = "2026-02-04T17:47:44.073Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/82/3cf75de64340829d55c87c36704f4d1d8c952bd2cdc8a7bc48cbfb8ab333/zensical-0.0.21-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7b380f545adb6d40896f9bd698eb0e1540ed4258d35b83f55f91658d0fdae312", size = 12678537, upload-time = "2026-02-04T17:47:46.899Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/91/6f4938dceeaa241f78bbfaf58a94acef10ba18be3468795173e3087abeb6/zensical-0.0.21-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c2227fdab64616bea94b40b8340bafe00e2e23631cc58eeea1e7267167e6ac5", size = 12822164, upload-time = "2026-02-04T17:47:49.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/4e/a9c9d25ef0766f767db7b4f09da68da9b3d8a28c3d68cfae01f8e3f9e297/zensical-0.0.21-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2e0f5154d236ed0f98662ee68785b67e8cd2138ea9d5e26070649e93c22eeee0", size = 12785632, upload-time = "2026-02-04T17:47:52.613Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zope-interface"
|
name = "zope-interface"
|
||||||
version = "8.2"
|
version = "8.2"
|
||||||
@@ -6268,34 +6147,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zensical"
|
|
||||||
version = "0.0.21"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "deepmerge", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/50/2655b5f72d0c72f4366be580f5e2354ff05280d047ea986fe89570e44589/zensical-0.0.21.tar.gz", hash = "sha256:c13563836fa63a3cabeffd83fe3a770ca740cfa5ae7b85df85d89837e31b3b4a", size = 3819731, upload-time = "2026-02-04T17:47:59.396Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/98/90710d232cb35b633815fa7b493da542391b89283b6103a5bb4ae9fc0dd9/zensical-0.0.21-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:67404cc70c330246dfb7269bcdb60a25be0bb60a212a09c9c50229a1341b1f84", size = 12237120, upload-time = "2026-02-04T17:47:28.615Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/fb/4280b3781157e8f051711732192f949bf29beeafd0df3e33c1c8bf9b7a1a/zensical-0.0.21-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4fd253ccfbf5af56434124f13bac01344e456c020148369b18d8836b6537c3c", size = 12118047, upload-time = "2026-02-04T17:47:31.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/b3/b7f85ae9cf920cf9f17bf157ae6c274919477148feb7716bf735636caa0e/zensical-0.0.21-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:440e40cdc30a29bf7466bcd6f43ed7bd1c54ea3f1a0fefca65619358b481a5bc", size = 12473440, upload-time = "2026-02-04T17:47:33.577Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/ac/1dc6e98f79ed19b9f103c88a0bd271f9140565d7d26b64bc1542b3ef6d91/zensical-0.0.21-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:368e832fc8068e75dc45cab59379db4cefcd81eb116f48d058db8fb7b7aa8d14", size = 12412588, upload-time = "2026-02-04T17:47:36.491Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/76/16a580f6dd32b387caa4a41615451e7dddd1917a2ff2e5b08744f41b4e11/zensical-0.0.21-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ab962d47f9dd73510eed168469326c7a452554dfbfdb9cdf85efc7140244df", size = 12749438, upload-time = "2026-02-04T17:47:38.969Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/30/4baaa1c910eee61db5f49d0d45f2e550a0027218c618f3dd7f8da966a019/zensical-0.0.21-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b846d53dfce007f056ff31848f87f3f2a388228e24d4851c0cafdce0fa204c9b", size = 12514504, upload-time = "2026-02-04T17:47:41.31Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/77/931fccae5580b94409a0448a26106f922dcfa7822e7b93cacd2876dd63a8/zensical-0.0.21-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:daac1075552d230d52d621d2e4754ba24d5afcaa201a7a991f1a8d57e320c9de", size = 12647832, upload-time = "2026-02-04T17:47:44.073Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/82/3cf75de64340829d55c87c36704f4d1d8c952bd2cdc8a7bc48cbfb8ab333/zensical-0.0.21-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7b380f545adb6d40896f9bd698eb0e1540ed4258d35b83f55f91658d0fdae312", size = 12678537, upload-time = "2026-02-04T17:47:46.899Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/91/6f4938dceeaa241f78bbfaf58a94acef10ba18be3468795173e3087abeb6/zensical-0.0.21-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c2227fdab64616bea94b40b8340bafe00e2e23631cc58eeea1e7267167e6ac5", size = 12822164, upload-time = "2026-02-04T17:47:49.231Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/4e/a9c9d25ef0766f767db7b4f09da68da9b3d8a28c3d68cfae01f8e3f9e297/zensical-0.0.21-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2e0f5154d236ed0f98662ee68785b67e8cd2138ea9d5e26070649e93c22eeee0", size = 12785632, upload-time = "2026-02-04T17:47:52.613Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstandard"
|
name = "zstandard"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user