mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-12 21:44:21 -06:00
Compare commits
1 Commits
dependabot
...
feature-tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e1fd1dafa |
@@ -414,6 +414,9 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
return newFilter
|
||||
}
|
||||
|
||||
const correspondentAny = addFilterOfType(TriggerFilterType.CorrespondentAny)
|
||||
correspondentAny.get('values').setValue([11])
|
||||
|
||||
const correspondentIs = addFilterOfType(TriggerFilterType.CorrespondentIs)
|
||||
correspondentIs.get('values').setValue(1)
|
||||
|
||||
@@ -423,12 +426,18 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
const documentTypeIs = addFilterOfType(TriggerFilterType.DocumentTypeIs)
|
||||
documentTypeIs.get('values').setValue(1)
|
||||
|
||||
const documentTypeAny = addFilterOfType(TriggerFilterType.DocumentTypeAny)
|
||||
documentTypeAny.get('values').setValue([12])
|
||||
|
||||
const documentTypeNot = addFilterOfType(TriggerFilterType.DocumentTypeNot)
|
||||
documentTypeNot.get('values').setValue([1])
|
||||
|
||||
const storagePathIs = addFilterOfType(TriggerFilterType.StoragePathIs)
|
||||
storagePathIs.get('values').setValue(1)
|
||||
|
||||
const storagePathAny = addFilterOfType(TriggerFilterType.StoragePathAny)
|
||||
storagePathAny.get('values').setValue([13])
|
||||
|
||||
const storagePathNot = addFilterOfType(TriggerFilterType.StoragePathNot)
|
||||
storagePathNot.get('values').setValue([1])
|
||||
|
||||
@@ -443,10 +452,13 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
expect(formValues.triggers[0].filter_has_tags).toEqual([1])
|
||||
expect(formValues.triggers[0].filter_has_all_tags).toEqual([2, 3])
|
||||
expect(formValues.triggers[0].filter_has_not_tags).toEqual([4])
|
||||
expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([11])
|
||||
expect(formValues.triggers[0].filter_has_correspondent).toEqual(1)
|
||||
expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([1])
|
||||
expect(formValues.triggers[0].filter_has_any_document_types).toEqual([12])
|
||||
expect(formValues.triggers[0].filter_has_document_type).toEqual(1)
|
||||
expect(formValues.triggers[0].filter_has_not_document_types).toEqual([1])
|
||||
expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([13])
|
||||
expect(formValues.triggers[0].filter_has_storage_path).toEqual(1)
|
||||
expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([1])
|
||||
expect(formValues.triggers[0].filter_custom_field_query).toEqual(
|
||||
@@ -509,16 +521,22 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
|
||||
setFilter(TriggerFilterType.TagsAll, 11)
|
||||
setFilter(TriggerFilterType.TagsNone, 12)
|
||||
setFilter(TriggerFilterType.CorrespondentAny, 16)
|
||||
setFilter(TriggerFilterType.CorrespondentNot, 13)
|
||||
setFilter(TriggerFilterType.DocumentTypeAny, 17)
|
||||
setFilter(TriggerFilterType.DocumentTypeNot, 14)
|
||||
setFilter(TriggerFilterType.StoragePathAny, 18)
|
||||
setFilter(TriggerFilterType.StoragePathNot, 15)
|
||||
|
||||
const formValues = component['getFormValues']()
|
||||
|
||||
expect(formValues.triggers[0].filter_has_all_tags).toEqual([11])
|
||||
expect(formValues.triggers[0].filter_has_not_tags).toEqual([12])
|
||||
expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([16])
|
||||
expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([13])
|
||||
expect(formValues.triggers[0].filter_has_any_document_types).toEqual([17])
|
||||
expect(formValues.triggers[0].filter_has_not_document_types).toEqual([14])
|
||||
expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([18])
|
||||
expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([15])
|
||||
})
|
||||
|
||||
@@ -642,8 +660,11 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
filter_has_tags: [],
|
||||
filter_has_all_tags: [],
|
||||
filter_has_not_tags: [],
|
||||
filter_has_any_correspondents: [],
|
||||
filter_has_not_correspondents: [],
|
||||
filter_has_any_document_types: [],
|
||||
filter_has_not_document_types: [],
|
||||
filter_has_any_storage_paths: [],
|
||||
filter_has_not_storage_paths: [],
|
||||
filter_has_correspondent: null,
|
||||
filter_has_document_type: null,
|
||||
@@ -701,11 +722,14 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
trigger.filter_has_tags = [1]
|
||||
trigger.filter_has_all_tags = [2, 3]
|
||||
trigger.filter_has_not_tags = [4]
|
||||
trigger.filter_has_any_correspondents = [10] as any
|
||||
trigger.filter_has_correspondent = 5 as any
|
||||
trigger.filter_has_not_correspondents = [6] as any
|
||||
trigger.filter_has_document_type = 7 as any
|
||||
trigger.filter_has_any_document_types = [11] as any
|
||||
trigger.filter_has_not_document_types = [8] as any
|
||||
trigger.filter_has_storage_path = 9 as any
|
||||
trigger.filter_has_any_storage_paths = [12] as any
|
||||
trigger.filter_has_not_storage_paths = [10] as any
|
||||
trigger.filter_custom_field_query = JSON.stringify([
|
||||
'AND',
|
||||
@@ -716,8 +740,8 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
component.ngOnInit()
|
||||
const triggerGroup = component.triggerFields.at(0) as FormGroup
|
||||
const filters = component.getFiltersFormArray(triggerGroup)
|
||||
expect(filters.length).toBe(10)
|
||||
const customFieldFilter = filters.at(9) as FormGroup
|
||||
expect(filters.length).toBe(13)
|
||||
const customFieldFilter = filters.at(12) as FormGroup
|
||||
expect(customFieldFilter.get('type').value).toBe(
|
||||
TriggerFilterType.CustomFieldQuery
|
||||
)
|
||||
@@ -726,12 +750,27 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
})
|
||||
|
||||
it('should expose select metadata helpers', () => {
|
||||
expect(component.isSelectMultiple(TriggerFilterType.CorrespondentAny)).toBe(
|
||||
true
|
||||
)
|
||||
expect(component.isSelectMultiple(TriggerFilterType.CorrespondentNot)).toBe(
|
||||
true
|
||||
)
|
||||
expect(component.isSelectMultiple(TriggerFilterType.CorrespondentIs)).toBe(
|
||||
false
|
||||
)
|
||||
expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeAny)).toBe(
|
||||
true
|
||||
)
|
||||
expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeIs)).toBe(
|
||||
false
|
||||
)
|
||||
expect(component.isSelectMultiple(TriggerFilterType.StoragePathAny)).toBe(
|
||||
true
|
||||
)
|
||||
expect(component.isSelectMultiple(TriggerFilterType.StoragePathIs)).toBe(
|
||||
false
|
||||
)
|
||||
|
||||
component.correspondents = [{ id: 1, name: 'C1' } as any]
|
||||
component.documentTypes = [{ id: 2, name: 'DT' } as any]
|
||||
@@ -743,9 +782,15 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
expect(
|
||||
component.getFilterSelectItems(TriggerFilterType.DocumentTypeIs)
|
||||
).toEqual(component.documentTypes)
|
||||
expect(
|
||||
component.getFilterSelectItems(TriggerFilterType.DocumentTypeAny)
|
||||
).toEqual(component.documentTypes)
|
||||
expect(
|
||||
component.getFilterSelectItems(TriggerFilterType.StoragePathIs)
|
||||
).toEqual(component.storagePaths)
|
||||
expect(
|
||||
component.getFilterSelectItems(TriggerFilterType.StoragePathAny)
|
||||
).toEqual(component.storagePaths)
|
||||
expect(component.getFilterSelectItems(TriggerFilterType.TagsAll)).toEqual(
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -145,10 +145,13 @@ export enum TriggerFilterType {
|
||||
TagsAny = 'tags_any',
|
||||
TagsAll = 'tags_all',
|
||||
TagsNone = 'tags_none',
|
||||
CorrespondentAny = 'correspondent_any',
|
||||
CorrespondentIs = 'correspondent_is',
|
||||
CorrespondentNot = 'correspondent_not',
|
||||
DocumentTypeAny = 'document_type_any',
|
||||
DocumentTypeIs = 'document_type_is',
|
||||
DocumentTypeNot = 'document_type_not',
|
||||
StoragePathAny = 'storage_path_any',
|
||||
StoragePathIs = 'storage_path_is',
|
||||
StoragePathNot = 'storage_path_not',
|
||||
CustomFieldQuery = 'custom_field_query',
|
||||
@@ -172,8 +175,11 @@ type TriggerFilterAggregate = {
|
||||
filter_has_tags: number[]
|
||||
filter_has_all_tags: number[]
|
||||
filter_has_not_tags: number[]
|
||||
filter_has_any_correspondents: number[]
|
||||
filter_has_not_correspondents: number[]
|
||||
filter_has_any_document_types: number[]
|
||||
filter_has_not_document_types: number[]
|
||||
filter_has_any_storage_paths: number[]
|
||||
filter_has_not_storage_paths: number[]
|
||||
filter_has_correspondent: number | null
|
||||
filter_has_document_type: number | null
|
||||
@@ -219,6 +225,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [
|
||||
allowMultipleEntries: false,
|
||||
allowMultipleValues: true,
|
||||
},
|
||||
{
|
||||
id: TriggerFilterType.CorrespondentAny,
|
||||
name: $localize`Has any of these correspondents`,
|
||||
inputType: 'select',
|
||||
allowMultipleEntries: false,
|
||||
allowMultipleValues: true,
|
||||
selectItems: 'correspondents',
|
||||
},
|
||||
{
|
||||
id: TriggerFilterType.CorrespondentIs,
|
||||
name: $localize`Has correspondent`,
|
||||
@@ -243,6 +257,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [
|
||||
allowMultipleValues: false,
|
||||
selectItems: 'documentTypes',
|
||||
},
|
||||
{
|
||||
id: TriggerFilterType.DocumentTypeAny,
|
||||
name: $localize`Has any of these document types`,
|
||||
inputType: 'select',
|
||||
allowMultipleEntries: false,
|
||||
allowMultipleValues: true,
|
||||
selectItems: 'documentTypes',
|
||||
},
|
||||
{
|
||||
id: TriggerFilterType.DocumentTypeNot,
|
||||
name: $localize`Does not have document types`,
|
||||
@@ -259,6 +281,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [
|
||||
allowMultipleValues: false,
|
||||
selectItems: 'storagePaths',
|
||||
},
|
||||
{
|
||||
id: TriggerFilterType.StoragePathAny,
|
||||
name: $localize`Has any of these storage paths`,
|
||||
inputType: 'select',
|
||||
allowMultipleEntries: false,
|
||||
allowMultipleValues: true,
|
||||
selectItems: 'storagePaths',
|
||||
},
|
||||
{
|
||||
id: TriggerFilterType.StoragePathNot,
|
||||
name: $localize`Does not have storage paths`,
|
||||
@@ -306,6 +336,15 @@ const FILTER_HANDLERS: Record<TriggerFilterType, FilterHandler> = {
|
||||
extract: (trigger) => trigger.filter_has_not_tags,
|
||||
hasValue: (value) => Array.isArray(value) && value.length > 0,
|
||||
},
|
||||
[TriggerFilterType.CorrespondentAny]: {
|
||||
apply: (aggregate, values) => {
|
||||
aggregate.filter_has_any_correspondents = Array.isArray(values)
|
||||
? [...values]
|
||||
: [values]
|
||||
},
|
||||
extract: (trigger) => trigger.filter_has_any_correspondents,
|
||||
hasValue: (value) => Array.isArray(value) && value.length > 0,
|
||||
},
|
||||
[TriggerFilterType.CorrespondentIs]: {
|
||||
apply: (aggregate, values) => {
|
||||
aggregate.filter_has_correspondent = Array.isArray(values)
|
||||
@@ -333,6 +372,15 @@ const FILTER_HANDLERS: Record<TriggerFilterType, FilterHandler> = {
|
||||
extract: (trigger) => trigger.filter_has_document_type,
|
||||
hasValue: (value) => value !== null && value !== undefined,
|
||||
},
|
||||
[TriggerFilterType.DocumentTypeAny]: {
|
||||
apply: (aggregate, values) => {
|
||||
aggregate.filter_has_any_document_types = Array.isArray(values)
|
||||
? [...values]
|
||||
: [values]
|
||||
},
|
||||
extract: (trigger) => trigger.filter_has_any_document_types,
|
||||
hasValue: (value) => Array.isArray(value) && value.length > 0,
|
||||
},
|
||||
[TriggerFilterType.DocumentTypeNot]: {
|
||||
apply: (aggregate, values) => {
|
||||
aggregate.filter_has_not_document_types = Array.isArray(values)
|
||||
@@ -351,6 +399,15 @@ const FILTER_HANDLERS: Record<TriggerFilterType, FilterHandler> = {
|
||||
extract: (trigger) => trigger.filter_has_storage_path,
|
||||
hasValue: (value) => value !== null && value !== undefined,
|
||||
},
|
||||
[TriggerFilterType.StoragePathAny]: {
|
||||
apply: (aggregate, values) => {
|
||||
aggregate.filter_has_any_storage_paths = Array.isArray(values)
|
||||
? [...values]
|
||||
: [values]
|
||||
},
|
||||
extract: (trigger) => trigger.filter_has_any_storage_paths,
|
||||
hasValue: (value) => Array.isArray(value) && value.length > 0,
|
||||
},
|
||||
[TriggerFilterType.StoragePathNot]: {
|
||||
apply: (aggregate, values) => {
|
||||
aggregate.filter_has_not_storage_paths = Array.isArray(values)
|
||||
@@ -642,8 +699,11 @@ export class WorkflowEditDialogComponent
|
||||
filter_has_tags: [],
|
||||
filter_has_all_tags: [],
|
||||
filter_has_not_tags: [],
|
||||
filter_has_any_correspondents: [],
|
||||
filter_has_not_correspondents: [],
|
||||
filter_has_any_document_types: [],
|
||||
filter_has_not_document_types: [],
|
||||
filter_has_any_storage_paths: [],
|
||||
filter_has_not_storage_paths: [],
|
||||
filter_has_correspondent: null,
|
||||
filter_has_document_type: null,
|
||||
@@ -670,10 +730,16 @@ export class WorkflowEditDialogComponent
|
||||
trigger.filter_has_tags = aggregate.filter_has_tags
|
||||
trigger.filter_has_all_tags = aggregate.filter_has_all_tags
|
||||
trigger.filter_has_not_tags = aggregate.filter_has_not_tags
|
||||
trigger.filter_has_any_correspondents =
|
||||
aggregate.filter_has_any_correspondents
|
||||
trigger.filter_has_not_correspondents =
|
||||
aggregate.filter_has_not_correspondents
|
||||
trigger.filter_has_any_document_types =
|
||||
aggregate.filter_has_any_document_types
|
||||
trigger.filter_has_not_document_types =
|
||||
aggregate.filter_has_not_document_types
|
||||
trigger.filter_has_any_storage_paths =
|
||||
aggregate.filter_has_any_storage_paths
|
||||
trigger.filter_has_not_storage_paths =
|
||||
aggregate.filter_has_not_storage_paths
|
||||
trigger.filter_has_correspondent =
|
||||
@@ -856,8 +922,11 @@ export class WorkflowEditDialogComponent
|
||||
case TriggerFilterType.TagsAny:
|
||||
case TriggerFilterType.TagsAll:
|
||||
case TriggerFilterType.TagsNone:
|
||||
case TriggerFilterType.CorrespondentAny:
|
||||
case TriggerFilterType.CorrespondentNot:
|
||||
case TriggerFilterType.DocumentTypeAny:
|
||||
case TriggerFilterType.DocumentTypeNot:
|
||||
case TriggerFilterType.StoragePathAny:
|
||||
case TriggerFilterType.StoragePathNot:
|
||||
return true
|
||||
default:
|
||||
@@ -1179,8 +1248,11 @@ export class WorkflowEditDialogComponent
|
||||
filter_has_tags: [],
|
||||
filter_has_all_tags: [],
|
||||
filter_has_not_tags: [],
|
||||
filter_has_any_correspondents: [],
|
||||
filter_has_not_correspondents: [],
|
||||
filter_has_any_document_types: [],
|
||||
filter_has_not_document_types: [],
|
||||
filter_has_any_storage_paths: [],
|
||||
filter_has_not_storage_paths: [],
|
||||
filter_custom_field_query: null,
|
||||
filter_has_correspondent: null,
|
||||
|
||||
@@ -44,10 +44,16 @@ export interface WorkflowTrigger extends ObjectWithId {
|
||||
|
||||
filter_has_not_tags?: number[] // Tag.id[]
|
||||
|
||||
filter_has_any_correspondents?: number[] // Correspondent.id[]
|
||||
|
||||
filter_has_not_correspondents?: number[] // Correspondent.id[]
|
||||
|
||||
filter_has_any_document_types?: number[] // DocumentType.id[]
|
||||
|
||||
filter_has_not_document_types?: number[] // DocumentType.id[]
|
||||
|
||||
filter_has_any_storage_paths?: number[] // StoragePath.id[]
|
||||
|
||||
filter_has_not_storage_paths?: number[] // StoragePath.id[]
|
||||
|
||||
filter_custom_field_query?: string
|
||||
|
||||
@@ -403,6 +403,18 @@ def existing_document_matches_workflow(
|
||||
f"Document tags {list(document.tags.all())} include excluded tags {list(trigger_has_not_tags_qs)}",
|
||||
)
|
||||
|
||||
allowed_correspondent_ids = set(
|
||||
trigger.filter_has_any_correspondents.values_list("id", flat=True),
|
||||
)
|
||||
if (
|
||||
allowed_correspondent_ids
|
||||
and document.correspondent_id not in allowed_correspondent_ids
|
||||
):
|
||||
return (
|
||||
False,
|
||||
f"Document correspondent {document.correspondent} is not one of {list(trigger.filter_has_any_correspondents.all())}",
|
||||
)
|
||||
|
||||
# Document correspondent vs trigger has_correspondent
|
||||
if (
|
||||
trigger.filter_has_correspondent_id is not None
|
||||
@@ -424,6 +436,17 @@ def existing_document_matches_workflow(
|
||||
f"Document correspondent {document.correspondent} is excluded by {list(trigger.filter_has_not_correspondents.all())}",
|
||||
)
|
||||
|
||||
allowed_document_type_ids = set(
|
||||
trigger.filter_has_any_document_types.values_list("id", flat=True),
|
||||
)
|
||||
if allowed_document_type_ids and (
|
||||
document.document_type_id not in allowed_document_type_ids
|
||||
):
|
||||
return (
|
||||
False,
|
||||
f"Document doc type {document.document_type} is not one of {list(trigger.filter_has_any_document_types.all())}",
|
||||
)
|
||||
|
||||
# Document document_type vs trigger has_document_type
|
||||
if (
|
||||
trigger.filter_has_document_type_id is not None
|
||||
@@ -445,6 +468,17 @@ def existing_document_matches_workflow(
|
||||
f"Document doc type {document.document_type} is excluded by {list(trigger.filter_has_not_document_types.all())}",
|
||||
)
|
||||
|
||||
allowed_storage_path_ids = set(
|
||||
trigger.filter_has_any_storage_paths.values_list("id", flat=True),
|
||||
)
|
||||
if allowed_storage_path_ids and (
|
||||
document.storage_path_id not in allowed_storage_path_ids
|
||||
):
|
||||
return (
|
||||
False,
|
||||
f"Document storage path {document.storage_path} is not one of {list(trigger.filter_has_any_storage_paths.all())}",
|
||||
)
|
||||
|
||||
# Document storage_path vs trigger has_storage_path
|
||||
if (
|
||||
trigger.filter_has_storage_path_id is not None
|
||||
@@ -532,6 +566,10 @@ def prefilter_documents_by_workflowtrigger(
|
||||
|
||||
# Correspondent, DocumentType, etc. filtering
|
||||
|
||||
if trigger.filter_has_any_correspondents.exists():
|
||||
documents = documents.filter(
|
||||
correspondent__in=trigger.filter_has_any_correspondents.all(),
|
||||
)
|
||||
if trigger.filter_has_correspondent is not None:
|
||||
documents = documents.filter(
|
||||
correspondent=trigger.filter_has_correspondent,
|
||||
@@ -541,6 +579,10 @@ def prefilter_documents_by_workflowtrigger(
|
||||
correspondent__in=trigger.filter_has_not_correspondents.all(),
|
||||
)
|
||||
|
||||
if trigger.filter_has_any_document_types.exists():
|
||||
documents = documents.filter(
|
||||
document_type__in=trigger.filter_has_any_document_types.all(),
|
||||
)
|
||||
if trigger.filter_has_document_type is not None:
|
||||
documents = documents.filter(
|
||||
document_type=trigger.filter_has_document_type,
|
||||
@@ -550,6 +592,10 @@ def prefilter_documents_by_workflowtrigger(
|
||||
document_type__in=trigger.filter_has_not_document_types.all(),
|
||||
)
|
||||
|
||||
if trigger.filter_has_any_storage_paths.exists():
|
||||
documents = documents.filter(
|
||||
storage_path__in=trigger.filter_has_any_storage_paths.all(),
|
||||
)
|
||||
if trigger.filter_has_storage_path is not None:
|
||||
documents = documents.filter(
|
||||
storage_path=trigger.filter_has_storage_path,
|
||||
@@ -604,8 +650,11 @@ def document_matches_workflow(
|
||||
"filter_has_tags",
|
||||
"filter_has_all_tags",
|
||||
"filter_has_not_tags",
|
||||
"filter_has_any_document_types",
|
||||
"filter_has_not_document_types",
|
||||
"filter_has_any_correspondents",
|
||||
"filter_has_not_correspondents",
|
||||
"filter_has_any_storage_paths",
|
||||
"filter_has_not_storage_paths",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.7 on 2025-12-17 22:25
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="workflowtrigger",
|
||||
name="filter_has_any_correspondents",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_any_correspondent",
|
||||
to="documents.correspondent",
|
||||
verbose_name="has one of these correspondents",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workflowtrigger",
|
||||
name="filter_has_any_document_types",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_any_document_type",
|
||||
to="documents.documenttype",
|
||||
verbose_name="has one of these document types",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workflowtrigger",
|
||||
name="filter_has_any_storage_paths",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_any_storage_path",
|
||||
to="documents.storagepath",
|
||||
verbose_name="has one of these storage paths",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1087,6 +1087,13 @@ class WorkflowTrigger(models.Model):
|
||||
verbose_name=_("has this document type"),
|
||||
)
|
||||
|
||||
filter_has_any_document_types = models.ManyToManyField(
|
||||
DocumentType,
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_any_document_type",
|
||||
verbose_name=_("has one of these document types"),
|
||||
)
|
||||
|
||||
filter_has_not_document_types = models.ManyToManyField(
|
||||
DocumentType,
|
||||
blank=True,
|
||||
@@ -1109,6 +1116,13 @@ class WorkflowTrigger(models.Model):
|
||||
verbose_name=_("does not have these correspondent(s)"),
|
||||
)
|
||||
|
||||
filter_has_any_correspondents = models.ManyToManyField(
|
||||
Correspondent,
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_any_correspondent",
|
||||
verbose_name=_("has one of these correspondents"),
|
||||
)
|
||||
|
||||
filter_has_storage_path = models.ForeignKey(
|
||||
StoragePath,
|
||||
null=True,
|
||||
@@ -1117,6 +1131,13 @@ class WorkflowTrigger(models.Model):
|
||||
verbose_name=_("has this storage path"),
|
||||
)
|
||||
|
||||
filter_has_any_storage_paths = models.ManyToManyField(
|
||||
StoragePath,
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_any_storage_path",
|
||||
verbose_name=_("has one of these storage paths"),
|
||||
)
|
||||
|
||||
filter_has_not_storage_paths = models.ManyToManyField(
|
||||
StoragePath,
|
||||
blank=True,
|
||||
|
||||
@@ -580,34 +580,30 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
||||
),
|
||||
)
|
||||
def get_children(self, obj):
|
||||
children_map = self.context.get("children_map")
|
||||
if children_map is not None:
|
||||
children = children_map.get(obj.pk, [])
|
||||
else:
|
||||
filter_q = self.context.get("document_count_filter")
|
||||
request = self.context.get("request")
|
||||
if filter_q is None:
|
||||
user = getattr(request, "user", None) if request else None
|
||||
filter_q = get_document_count_filter_for_user(user)
|
||||
self.context["document_count_filter"] = filter_q
|
||||
filter_q = self.context.get("document_count_filter")
|
||||
request = self.context.get("request")
|
||||
if filter_q is None:
|
||||
user = getattr(request, "user", None) if request else None
|
||||
filter_q = get_document_count_filter_for_user(user)
|
||||
self.context["document_count_filter"] = filter_q
|
||||
|
||||
children = (
|
||||
obj.get_children_queryset()
|
||||
.select_related("owner")
|
||||
.annotate(document_count=Count("documents", filter=filter_q))
|
||||
)
|
||||
children_queryset = (
|
||||
obj.get_children_queryset()
|
||||
.select_related("owner")
|
||||
.annotate(document_count=Count("documents", filter=filter_q))
|
||||
)
|
||||
|
||||
view = self.context.get("view")
|
||||
ordering = (
|
||||
OrderingFilter().get_ordering(request, children, view)
|
||||
if request and view
|
||||
else None
|
||||
)
|
||||
ordering = ordering or (Lower("name"),)
|
||||
children = children.order_by(*ordering)
|
||||
view = self.context.get("view")
|
||||
ordering = (
|
||||
OrderingFilter().get_ordering(request, children_queryset, view)
|
||||
if request and view
|
||||
else None
|
||||
)
|
||||
ordering = ordering or (Lower("name"),)
|
||||
children_queryset = children_queryset.order_by(*ordering)
|
||||
|
||||
serializer = TagSerializer(
|
||||
children,
|
||||
children_queryset,
|
||||
many=True,
|
||||
user=self.user,
|
||||
full_perms=self.full_perms,
|
||||
@@ -2299,8 +2295,11 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
||||
"filter_has_all_tags",
|
||||
"filter_has_not_tags",
|
||||
"filter_custom_field_query",
|
||||
"filter_has_any_correspondents",
|
||||
"filter_has_not_correspondents",
|
||||
"filter_has_any_document_types",
|
||||
"filter_has_not_document_types",
|
||||
"filter_has_any_storage_paths",
|
||||
"filter_has_not_storage_paths",
|
||||
"filter_has_correspondent",
|
||||
"filter_has_document_type",
|
||||
@@ -2538,14 +2537,26 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
filter_has_tags = trigger.pop("filter_has_tags", None)
|
||||
filter_has_all_tags = trigger.pop("filter_has_all_tags", None)
|
||||
filter_has_not_tags = trigger.pop("filter_has_not_tags", None)
|
||||
filter_has_any_correspondents = trigger.pop(
|
||||
"filter_has_any_correspondents",
|
||||
None,
|
||||
)
|
||||
filter_has_not_correspondents = trigger.pop(
|
||||
"filter_has_not_correspondents",
|
||||
None,
|
||||
)
|
||||
filter_has_any_document_types = trigger.pop(
|
||||
"filter_has_any_document_types",
|
||||
None,
|
||||
)
|
||||
filter_has_not_document_types = trigger.pop(
|
||||
"filter_has_not_document_types",
|
||||
None,
|
||||
)
|
||||
filter_has_any_storage_paths = trigger.pop(
|
||||
"filter_has_any_storage_paths",
|
||||
None,
|
||||
)
|
||||
filter_has_not_storage_paths = trigger.pop(
|
||||
"filter_has_not_storage_paths",
|
||||
None,
|
||||
@@ -2562,14 +2573,26 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
trigger_instance.filter_has_all_tags.set(filter_has_all_tags)
|
||||
if filter_has_not_tags is not None:
|
||||
trigger_instance.filter_has_not_tags.set(filter_has_not_tags)
|
||||
if filter_has_any_correspondents is not None:
|
||||
trigger_instance.filter_has_any_correspondents.set(
|
||||
filter_has_any_correspondents,
|
||||
)
|
||||
if filter_has_not_correspondents is not None:
|
||||
trigger_instance.filter_has_not_correspondents.set(
|
||||
filter_has_not_correspondents,
|
||||
)
|
||||
if filter_has_any_document_types is not None:
|
||||
trigger_instance.filter_has_any_document_types.set(
|
||||
filter_has_any_document_types,
|
||||
)
|
||||
if filter_has_not_document_types is not None:
|
||||
trigger_instance.filter_has_not_document_types.set(
|
||||
filter_has_not_document_types,
|
||||
)
|
||||
if filter_has_any_storage_paths is not None:
|
||||
trigger_instance.filter_has_any_storage_paths.set(
|
||||
filter_has_any_storage_paths,
|
||||
)
|
||||
if filter_has_not_storage_paths is not None:
|
||||
trigger_instance.filter_has_not_storage_paths.set(
|
||||
filter_has_not_storage_paths,
|
||||
|
||||
@@ -186,8 +186,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
"filter_has_tags": [self.t1.id],
|
||||
"filter_has_all_tags": [self.t2.id],
|
||||
"filter_has_not_tags": [self.t3.id],
|
||||
"filter_has_any_correspondents": [self.c.id],
|
||||
"filter_has_not_correspondents": [self.c2.id],
|
||||
"filter_has_any_document_types": [self.dt.id],
|
||||
"filter_has_not_document_types": [self.dt2.id],
|
||||
"filter_has_any_storage_paths": [self.sp.id],
|
||||
"filter_has_not_storage_paths": [self.sp2.id],
|
||||
"filter_custom_field_query": json.dumps(
|
||||
[
|
||||
@@ -248,14 +251,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
set(trigger.filter_has_not_tags.values_list("id", flat=True)),
|
||||
{self.t3.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_any_correspondents.values_list("id", flat=True)),
|
||||
{self.c.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_not_correspondents.values_list("id", flat=True)),
|
||||
{self.c2.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_any_document_types.values_list("id", flat=True)),
|
||||
{self.dt.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_not_document_types.values_list("id", flat=True)),
|
||||
{self.dt2.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_any_storage_paths.values_list("id", flat=True)),
|
||||
{self.sp.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_not_storage_paths.values_list("id", flat=True)),
|
||||
{self.sp2.id},
|
||||
@@ -419,8 +434,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
"filter_has_tags": [self.t1.id],
|
||||
"filter_has_all_tags": [self.t2.id],
|
||||
"filter_has_not_tags": [self.t3.id],
|
||||
"filter_has_any_correspondents": [self.c.id],
|
||||
"filter_has_not_correspondents": [self.c2.id],
|
||||
"filter_has_any_document_types": [self.dt.id],
|
||||
"filter_has_not_document_types": [self.dt2.id],
|
||||
"filter_has_any_storage_paths": [self.sp.id],
|
||||
"filter_has_not_storage_paths": [self.sp2.id],
|
||||
"filter_custom_field_query": json.dumps(
|
||||
["AND", [[self.cf1.id, "exact", "value"]]],
|
||||
@@ -450,14 +468,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
workflow.triggers.first().filter_has_not_tags.first(),
|
||||
self.t3,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_any_correspondents.first(),
|
||||
self.c,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_not_correspondents.first(),
|
||||
self.c2,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_any_document_types.first(),
|
||||
self.dt,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_not_document_types.first(),
|
||||
self.dt2,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_any_storage_paths.first(),
|
||||
self.sp,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_not_storage_paths.first(),
|
||||
self.sp2,
|
||||
|
||||
@@ -1276,6 +1276,76 @@ class TestWorkflows(
|
||||
)
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
def test_document_added_any_filters(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
)
|
||||
trigger.filter_has_any_correspondents.set([self.c])
|
||||
trigger.filter_has_any_document_types.set([self.dt])
|
||||
trigger.filter_has_any_storage_paths.set([self.sp])
|
||||
|
||||
matching_doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
document_type=self.dt,
|
||||
storage_path=self.sp,
|
||||
original_filename="sample.pdf",
|
||||
checksum="checksum-any-match",
|
||||
)
|
||||
|
||||
matched, reason = existing_document_matches_workflow(matching_doc, trigger)
|
||||
self.assertTrue(matched)
|
||||
self.assertIsNone(reason)
|
||||
|
||||
wrong_correspondent = Document.objects.create(
|
||||
title="wrong correspondent",
|
||||
correspondent=self.c2,
|
||||
document_type=self.dt,
|
||||
storage_path=self.sp,
|
||||
original_filename="sample2.pdf",
|
||||
)
|
||||
matched, reason = existing_document_matches_workflow(
|
||||
wrong_correspondent,
|
||||
trigger,
|
||||
)
|
||||
self.assertFalse(matched)
|
||||
self.assertIn("correspondent", reason)
|
||||
|
||||
other_document_type = DocumentType.objects.create(name="Other")
|
||||
wrong_document_type = Document.objects.create(
|
||||
title="wrong doc type",
|
||||
correspondent=self.c,
|
||||
document_type=other_document_type,
|
||||
storage_path=self.sp,
|
||||
original_filename="sample3.pdf",
|
||||
checksum="checksum-wrong-doc-type",
|
||||
)
|
||||
matched, reason = existing_document_matches_workflow(
|
||||
wrong_document_type,
|
||||
trigger,
|
||||
)
|
||||
self.assertFalse(matched)
|
||||
self.assertIn("doc type", reason)
|
||||
|
||||
other_storage_path = StoragePath.objects.create(
|
||||
name="Other path",
|
||||
path="/other/",
|
||||
)
|
||||
wrong_storage_path = Document.objects.create(
|
||||
title="wrong storage",
|
||||
correspondent=self.c,
|
||||
document_type=self.dt,
|
||||
storage_path=other_storage_path,
|
||||
original_filename="sample4.pdf",
|
||||
checksum="checksum-wrong-storage-path",
|
||||
)
|
||||
matched, reason = existing_document_matches_workflow(
|
||||
wrong_storage_path,
|
||||
trigger,
|
||||
)
|
||||
self.assertFalse(matched)
|
||||
self.assertIn("storage path", reason)
|
||||
|
||||
def test_document_added_custom_field_query_no_match(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
@@ -1384,6 +1454,39 @@ class TestWorkflows(
|
||||
self.assertIn(doc1, filtered)
|
||||
self.assertNotIn(doc2, filtered)
|
||||
|
||||
def test_prefilter_documents_any_filters(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
)
|
||||
trigger.filter_has_any_correspondents.set([self.c])
|
||||
trigger.filter_has_any_document_types.set([self.dt])
|
||||
trigger.filter_has_any_storage_paths.set([self.sp])
|
||||
|
||||
allowed_document = Document.objects.create(
|
||||
title="allowed",
|
||||
correspondent=self.c,
|
||||
document_type=self.dt,
|
||||
storage_path=self.sp,
|
||||
original_filename="doc-allowed.pdf",
|
||||
checksum="checksum-any-allowed",
|
||||
)
|
||||
blocked_document = Document.objects.create(
|
||||
title="blocked",
|
||||
correspondent=self.c2,
|
||||
document_type=self.dt,
|
||||
storage_path=self.sp,
|
||||
original_filename="doc-blocked.pdf",
|
||||
checksum="checksum-any-blocked",
|
||||
)
|
||||
|
||||
filtered = prefilter_documents_by_workflowtrigger(
|
||||
Document.objects.all(),
|
||||
trigger,
|
||||
)
|
||||
|
||||
self.assertIn(allowed_document, filtered)
|
||||
self.assertNotIn(blocked_document, filtered)
|
||||
|
||||
def test_consumption_trigger_requires_filter_configuration(self):
|
||||
serializer = WorkflowTriggerSerializer(
|
||||
data={
|
||||
|
||||
@@ -448,43 +448,8 @@ class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["document_count_filter"] = self.get_document_count_filter()
|
||||
if hasattr(self, "_children_map"):
|
||||
context["children_map"] = self._children_map
|
||||
return context
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Build a children map once to avoid per-parent queries in the serializer.
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
ordering = OrderingFilter().get_ordering(request, queryset, self) or (
|
||||
Lower("name"),
|
||||
)
|
||||
queryset = queryset.order_by(*ordering)
|
||||
|
||||
all_tags = list(queryset)
|
||||
descendant_pks = {pk for tag in all_tags for pk in tag.get_descendants_pks()}
|
||||
|
||||
if descendant_pks:
|
||||
filter_q = self.get_document_count_filter()
|
||||
children_source = (
|
||||
Tag.objects.filter(pk__in=descendant_pks | {t.pk for t in all_tags})
|
||||
.select_related("owner")
|
||||
.annotate(document_count=Count("documents", filter=filter_q))
|
||||
.order_by(*ordering)
|
||||
)
|
||||
else:
|
||||
children_source = all_tags
|
||||
|
||||
children_map = {}
|
||||
for tag in children_source:
|
||||
children_map.setdefault(tag.tn_parent_id, []).append(tag)
|
||||
self._children_map = children_map
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
old_parent = self.get_object().get_parent()
|
||||
tag = serializer.save()
|
||||
|
||||
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-12 21:04+0000\n"
|
||||
"POT-Creation-Date: 2026-01-08 21:50+0000\n"
|
||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@@ -1219,35 +1219,35 @@ msgstr ""
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:646
|
||||
#: documents/serialisers.py:642
|
||||
msgid "Invalid color."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1850
|
||||
#: documents/serialisers.py:1846
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1894
|
||||
#: documents/serialisers.py:1890
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1901
|
||||
#: documents/serialisers.py:1897
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1918 documents/serialisers.py:1928
|
||||
#: documents/serialisers.py:1914 documents/serialisers.py:1924
|
||||
msgid ""
|
||||
"Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1923
|
||||
#: documents/serialisers.py:1919
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2038
|
||||
#: documents/serialisers.py:2034
|
||||
msgid "Invalid variable detected."
|
||||
msgstr ""
|
||||
|
||||
|
||||
52
uv.lock
generated
52
uv.lock
generated
@@ -104,9 +104,9 @@ dependencies = [
|
||||
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -118,9 +118,9 @@ dependencies = [
|
||||
{ name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633, upload-time = "2025-04-03T23:51:02.058Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071, upload-time = "2025-04-03T23:51:03.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1483,9 +1483,9 @@ wheels = [
|
||||
name = "isodate"
|
||||
version = "0.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1832,7 +1832,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.7.1"
|
||||
version = "9.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -1847,9 +1847,9 @@ dependencies = [
|
||||
{ name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3543,25 +3543,25 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.11"
|
||||
version = "0.14.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user