mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'fix/issue-401' into feature/any-all-filtering
This commit is contained in:
commit
14d04ff8d2
5
crowdin.yml
Normal file
5
crowdin.yml
Normal file
@ -0,0 +1,5 @@
|
||||
files:
|
||||
- source: /src/locale/en-us/LC_MESSAGES/django.po
|
||||
translation: /src/locale/%two_letters_code%/LC_MESSAGES/django.po
|
||||
- source: /src-ui/messages.xlf
|
||||
translation: /src-ui/src/locale/messages.%two_letters_code%.xlf
|
@ -79,7 +79,11 @@ initialize() {
|
||||
fi
|
||||
done
|
||||
|
||||
echo "creating directory /tmp/paperless"
|
||||
mkdir -p /tmp/paperless
|
||||
|
||||
chown -R paperless:paperless ../
|
||||
chown -R paperless:paperless /tmp/paperless
|
||||
|
||||
migrations
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
Changelog
|
||||
*********
|
||||
|
||||
|
||||
paperless-ng 1.0.0
|
||||
##################
|
||||
|
||||
@ -25,7 +24,8 @@ Nothing special about this release, but since there are relatively few bug repor
|
||||
* Added a language selector to the settings.
|
||||
* Added date format options to the settings.
|
||||
* Range selection with shift clicking is now possible in the document list.
|
||||
|
||||
* Filtering correspondent, type and tag management pages by name.
|
||||
* Focus "Name" field in dialogs by default.
|
||||
|
||||
|
||||
paperless-ng 0.9.14
|
||||
|
@ -29,15 +29,22 @@
|
||||
</div>
|
||||
<div *ngIf="selectionModel.items" class="items">
|
||||
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)"></app-toggleable-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!selectionModel.isDirty()">
|
||||
<small class="ml-1" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
|
||||
<small class="ml-2" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
|
||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
</svg>
|
||||
</button>
|
||||
<div *ngIf="!editing" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||
<small i18n>Use
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-option" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#option" />
|
||||
</svg>
|
||||
+ click to exclude items.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,3 +35,15 @@
|
||||
.btn-group > label.disabled {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
|
||||
small > svg {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.list-group-item-note {
|
||||
line-height: 1;
|
||||
|
||||
small {
|
||||
font-size: 70%;
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,10 @@ export class FilterableDropdownSelectionModel {
|
||||
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
|
||||
}
|
||||
|
||||
getExcludedItems() {
|
||||
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded)
|
||||
}
|
||||
|
||||
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
||||
if (state == ToggleableItemState.NotSelected) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
@ -58,9 +62,9 @@ export class FilterableDropdownSelectionModel {
|
||||
|
||||
toggle(id: number, fireEvent = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (state == null || state != ToggleableItemState.Selected) {
|
||||
if (state == null || (state != ToggleableItemState.Selected && state != ToggleableItemState.Excluded)) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||
} else if (state == ToggleableItemState.Selected) {
|
||||
} else if (state == ToggleableItemState.Selected || state == ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
@ -85,7 +89,27 @@ export class FilterableDropdownSelectionModel {
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
|
||||
exclude(id: number, fireEvent:boolean = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (state == null || state != ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||
} else if (state == ToggleableItemState.Excluded) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
if (!this.multiple) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
|
||||
private getNonTemporary(id: number) {
|
||||
@ -287,4 +311,12 @@ export class FilterableDropdownComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
excludeClicked(itemID: number) {
|
||||
if (this.editing) {
|
||||
this.selectionModel.toggle(itemID)
|
||||
} else {
|
||||
this.selectionModel.exclude(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem($event)">
|
||||
<div class="selected-icon mr-1">
|
||||
<ng-container *ngIf="isChecked()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||
@ -10,10 +10,14 @@
|
||||
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isExcluded()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="mr-1">
|
||||
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></app-tag>
|
||||
<ng-template #displayName><small>{{item.name}}</small></ng-template>
|
||||
</div>
|
||||
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
|
||||
|
@ -4,7 +4,8 @@ import { MatchingModel } from 'src/app/data/matching-model';
|
||||
export enum ToggleableItemState {
|
||||
NotSelected = 0,
|
||||
Selected = 1,
|
||||
PartiallySelected = 2
|
||||
PartiallySelected = 2,
|
||||
Excluded = 3
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -26,12 +27,19 @@ export class ToggleableDropdownButtonComponent {
|
||||
@Output()
|
||||
toggle = new EventEmitter()
|
||||
|
||||
@Output()
|
||||
exclude = new EventEmitter()
|
||||
|
||||
get isTag(): boolean {
|
||||
return 'is_inbox_tag' in this.item
|
||||
}
|
||||
|
||||
toggleItem(): void {
|
||||
this.toggle.emit()
|
||||
toggleItem(event: MouseEvent): void {
|
||||
if (event.altKey) {
|
||||
this.exclude.emit()
|
||||
} else {
|
||||
this.toggle.emit()
|
||||
}
|
||||
}
|
||||
|
||||
isChecked() {
|
||||
@ -42,4 +50,7 @@ export class ToggleableDropdownButtonComponent {
|
||||
return this.state == ToggleableItemState.PartiallySelected
|
||||
}
|
||||
|
||||
isExcluded() {
|
||||
return this.state == ToggleableItemState.Excluded
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable" (click)="this.toggleSelected.emit($event)">
|
||||
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected">
|
||||
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left">
|
||||
|
||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="col p-2 h-100">
|
||||
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" (click)="this.toggleSelected.emit($event)">
|
||||
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected">
|
||||
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
|
||||
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)">
|
||||
<img class="card-img doc-img rounded-top" [src]="getThumbUrl()">
|
||||
|
||||
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
|
||||
|
@ -152,7 +152,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id)"></app-tag>
|
||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
<ng-container *ngIf="d.document_type">
|
||||
|
@ -8,7 +8,7 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
||||
import { TagService } from 'src/app/services/rest/tag.service';
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||
import { FilterRule } from 'src/app/data/filter-rule';
|
||||
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ANY, FILTER_TITLE } from 'src/app/data/filter-rule-type';
|
||||
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ANY, FILTER_DOES_NOT_HAVE_TAG, FILTER_TITLE } from 'src/app/data/filter-rule-type';
|
||||
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
|
||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||
|
||||
@ -111,6 +111,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
case FILTER_HAS_ANY_TAG:
|
||||
this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
|
||||
break
|
||||
case FILTER_DOES_NOT_HAVE_TAG:
|
||||
this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Excluded, false)
|
||||
break
|
||||
case FILTER_CORRESPONDENT:
|
||||
this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||
break
|
||||
@ -133,6 +136,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => {
|
||||
filterRules.push({rule_type: tagFilterType, value: tag.id?.toString()})
|
||||
})
|
||||
this.tagSelectionModel.getExcludedItems().filter(tag => tag.id).forEach(tag => {
|
||||
filterRules.push({rule_type: FILTER_DOES_NOT_HAVE_TAG, value: tag.id?.toString()})
|
||||
})
|
||||
}
|
||||
this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
|
||||
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()})
|
||||
|
@ -449,12 +449,20 @@
|
||||
<context context-type="linenumber">2</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="ddb40946e790522301687ecddb9ce1cb8ad40dd1">
|
||||
<source>Filter by:</source>
|
||||
<target>Filtern nach:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">8</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="cff1428d10d59d14e45edec3c735a27b5482db59">
|
||||
<source>Name</source>
|
||||
<target>Name</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="8fa4d523f7b91df4390120b85ed0406138273e1a">
|
||||
@ -462,7 +470,7 @@
|
||||
<target>Farbe</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">14</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="d0c4488f742efeba0915e90e285a022da813deff">
|
||||
@ -470,7 +478,7 @@
|
||||
<target>Zuweisung</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">21</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="9bcf8d20d23c111eca1431abd2d2ce0de324499c">
|
||||
@ -478,7 +486,7 @@
|
||||
<target>Anzahl Dokumente</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">16</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="030b4423b92167200e39519599f9b863b4f7c62c">
|
||||
@ -486,7 +494,7 @@
|
||||
<target>Aktionen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="9d51b3c90afda70700229d1b8a55371c13cb3bce">
|
||||
@ -494,7 +502,7 @@
|
||||
<target>Dokumente</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="28f86ffd419b869711aa13f5e5ff54be6d70731c">
|
||||
@ -502,7 +510,7 @@
|
||||
<target>Bearbeiten</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">37</context>
|
||||
<context context-type="linenumber">43</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="4990731724078522539">
|
||||
@ -814,7 +822,7 @@
|
||||
<target>Letzter Kontakt</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="1234709746630139322">
|
||||
@ -1114,14 +1122,6 @@
|
||||
<context context-type="linenumber">46</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="ddb40946e790522301687ecddb9ce1cb8ad40dd1">
|
||||
<source>Filter by:</source>
|
||||
<target>Filtern nach:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
|
||||
<source>Filter tags</source>
|
||||
<target>Tags filtern</target>
|
||||
@ -1907,7 +1907,7 @@
|
||||
<target>Automatisch</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">33</context>
|
||||
<context context-type="linenumber">39</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5044611416737085530">
|
||||
@ -1915,7 +1915,7 @@
|
||||
<target>Möchten Sie dieses Element wirklich löschen?</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">76</context>
|
||||
<context context-type="linenumber">97</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="8371896857609524947">
|
||||
@ -1923,7 +1923,7 @@
|
||||
<target>Assoziierte Dokumente werden nicht gelöscht.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">83</context>
|
||||
<context context-type="linenumber">104</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="7022070615528435141">
|
||||
@ -1931,7 +1931,7 @@
|
||||
<target>Löschen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5467489005440577210">
|
||||
@ -1939,7 +1939,7 @@
|
||||
<target>Fehler beim Löschen des Elements: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">93</context>
|
||||
<context context-type="linenumber">114</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5851669019930456395">
|
||||
|
@ -449,12 +449,20 @@
|
||||
<context context-type="linenumber">2</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="ddb40946e790522301687ecddb9ce1cb8ad40dd1">
|
||||
<source>Filter by:</source>
|
||||
<target>Filtrer par : </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">8</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="cff1428d10d59d14e45edec3c735a27b5482db59">
|
||||
<source>Name</source>
|
||||
<target>Nom</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="8fa4d523f7b91df4390120b85ed0406138273e1a">
|
||||
@ -462,7 +470,7 @@
|
||||
<target>Couleur</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">14</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="d0c4488f742efeba0915e90e285a022da813deff">
|
||||
@ -470,7 +478,7 @@
|
||||
<target>Rapprochement</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">21</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="9bcf8d20d23c111eca1431abd2d2ce0de324499c">
|
||||
@ -478,7 +486,7 @@
|
||||
<target>Nombre de documents</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">16</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="030b4423b92167200e39519599f9b863b4f7c62c">
|
||||
@ -486,7 +494,7 @@
|
||||
<target>Actions</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="9d51b3c90afda70700229d1b8a55371c13cb3bce">
|
||||
@ -494,7 +502,7 @@
|
||||
<target>Documents</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="28f86ffd419b869711aa13f5e5ff54be6d70731c">
|
||||
@ -502,7 +510,7 @@
|
||||
<target>Éditer</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">37</context>
|
||||
<context context-type="linenumber">43</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="4990731724078522539">
|
||||
@ -814,7 +822,7 @@
|
||||
<target>Dernière correspondance</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="1234709746630139322">
|
||||
@ -1114,14 +1122,6 @@
|
||||
<context context-type="linenumber">46</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="ddb40946e790522301687ecddb9ce1cb8ad40dd1">
|
||||
<source>Filter by:</source>
|
||||
<target>Filtrer par : </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
|
||||
<source>Filter tags</source>
|
||||
<target>Filtrer les étiquettes</target>
|
||||
@ -1907,7 +1907,7 @@
|
||||
<target>Automatique</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">33</context>
|
||||
<context context-type="linenumber">39</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5044611416737085530">
|
||||
@ -1915,7 +1915,7 @@
|
||||
<target>Voulez-vous vraiment supprimer cet élément ?</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">76</context>
|
||||
<context context-type="linenumber">97</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="8371896857609524947">
|
||||
@ -1923,7 +1923,7 @@
|
||||
<target>Les documents associés ne seront pas supprimés.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">83</context>
|
||||
<context context-type="linenumber">104</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="7022070615528435141">
|
||||
@ -1931,7 +1931,7 @@
|
||||
<target>Supprimer</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5467489005440577210">
|
||||
@ -1939,7 +1939,7 @@
|
||||
<target>Une erreur s'est produite lors de la suppression de l'élément : <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">93</context>
|
||||
<context context-type="linenumber">114</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5851669019930456395">
|
||||
|
@ -449,12 +449,20 @@
|
||||
<context context-type="linenumber">2</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="ddb40946e790522301687ecddb9ce1cb8ad40dd1">
|
||||
<source>Filter by:</source>
|
||||
<target>Filter op:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">8</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="cff1428d10d59d14e45edec3c735a27b5482db59">
|
||||
<source>Name</source>
|
||||
<target>Naam</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="8fa4d523f7b91df4390120b85ed0406138273e1a">
|
||||
@ -462,7 +470,7 @@
|
||||
<target>Kleur</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">14</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="d0c4488f742efeba0915e90e285a022da813deff">
|
||||
@ -470,7 +478,7 @@
|
||||
<target>Overeenkomend</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">21</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="9bcf8d20d23c111eca1431abd2d2ce0de324499c">
|
||||
@ -478,7 +486,7 @@
|
||||
<target>Aantal documenten</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">16</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="030b4423b92167200e39519599f9b863b4f7c62c">
|
||||
@ -486,7 +494,7 @@
|
||||
<target>Acties</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="9d51b3c90afda70700229d1b8a55371c13cb3bce">
|
||||
@ -494,7 +502,7 @@
|
||||
<target>Documenten</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
<context context-type="linenumber">38</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="28f86ffd419b869711aa13f5e5ff54be6d70731c">
|
||||
@ -502,7 +510,7 @@
|
||||
<target>Bewerk</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
|
||||
<context context-type="linenumber">37</context>
|
||||
<context context-type="linenumber">43</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="4990731724078522539">
|
||||
@ -814,7 +822,7 @@
|
||||
<target>Laatste correspondentie</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.html</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
<context context-type="linenumber">22</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="1234709746630139322">
|
||||
@ -1114,14 +1122,6 @@
|
||||
<context context-type="linenumber">46</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="ddb40946e790522301687ecddb9ce1cb8ad40dd1">
|
||||
<source>Filter by:</source>
|
||||
<target>Filter op:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
|
||||
<source>Filter tags</source>
|
||||
<target>Etiketten filteren</target>
|
||||
@ -1907,7 +1907,7 @@
|
||||
<target>Automatisch</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">33</context>
|
||||
<context context-type="linenumber">39</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5044611416737085530">
|
||||
@ -1915,7 +1915,7 @@
|
||||
<target>Wil je dit element echt verwijderen?</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">76</context>
|
||||
<context context-type="linenumber">97</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="8371896857609524947">
|
||||
@ -1923,7 +1923,7 @@
|
||||
<target>Geassocieerde documenten zullen niet verwijderd worden.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">83</context>
|
||||
<context context-type="linenumber">104</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="7022070615528435141">
|
||||
@ -1931,7 +1931,7 @@
|
||||
<target>Verwijderen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5467489005440577210">
|
||||
@ -1939,7 +1939,7 @@
|
||||
<target>Fout bij het verwijderen van het element: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/generic-list/generic-list.component.ts</context>
|
||||
<context context-type="linenumber">93</context>
|
||||
<context context-type="linenumber">114</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit datatype="html" id="5851669019930456395">
|
||||
|
@ -70,31 +70,6 @@ def _consume(filepath):
|
||||
"Error while consuming document: {}".format(e))
|
||||
|
||||
|
||||
def _test_inotify(directory):
|
||||
if not INotify:
|
||||
return False
|
||||
|
||||
test_file = os.path.join(directory, "__inotify_test_file__")
|
||||
inotify = INotify()
|
||||
descriptor = None
|
||||
try:
|
||||
inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO
|
||||
descriptor = inotify.add_watch(directory, inotify_flags)
|
||||
Path(test_file).touch()
|
||||
events = inotify.read(timeout=1000)
|
||||
return len(events) == 1
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error while checking inotify availability: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
if descriptor:
|
||||
inotify.rm_watch(descriptor)
|
||||
inotify.close()
|
||||
if os.path.isfile(test_file):
|
||||
os.unlink(test_file)
|
||||
|
||||
|
||||
def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
|
||||
mtime = -1
|
||||
current_try = 0
|
||||
@ -179,24 +154,16 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
if settings.CONSUMER_POLLING == 0:
|
||||
if _test_inotify(directory):
|
||||
self.handle_inotify(directory, recursive)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Inotify notifications are not available on {directory}, "
|
||||
f"falling back to polling every 10 seconds")
|
||||
self.handle_polling(
|
||||
directory, recursive, 10)
|
||||
self.handle_inotify(directory, recursive)
|
||||
else:
|
||||
self.handle_polling(
|
||||
directory, recursive, settings.CONSUMER_POLLING)
|
||||
self.handle_polling(directory, recursive)
|
||||
|
||||
logger.debug("Consumer exiting.")
|
||||
|
||||
def handle_polling(self, directory, recursive, timeout):
|
||||
def handle_polling(self, directory, recursive):
|
||||
logging.getLogger(__name__).info(
|
||||
f"Polling directory for changes: {directory}")
|
||||
self.observer = PollingObserver(timeout=timeout)
|
||||
self.observer = PollingObserver(timeout=settings.CONSUMER_POLLING)
|
||||
self.observer.schedule(Handler(), directory, recursive=recursive)
|
||||
self.observer.start()
|
||||
try:
|
||||
|
@ -63,12 +63,6 @@ class MatchingModel(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
self.match = self.match.lower()
|
||||
|
||||
models.Model.save(self, *args, **kwargs)
|
||||
|
||||
|
||||
class Correspondent(MatchingModel):
|
||||
|
||||
|
@ -923,6 +923,14 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
doc2 = Document.objects.get(id=self.doc2.id)
|
||||
self.assertEqual(doc2.correspondent, self.c1)
|
||||
|
||||
def test_api_no_correspondent(self):
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_correspondent",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_api_invalid_document_type(self):
|
||||
self.assertEqual(self.doc2.document_type, self.dt1)
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
@ -935,6 +943,14 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
doc2 = Document.objects.get(id=self.doc2.id)
|
||||
self.assertEqual(doc2.document_type, self.dt1)
|
||||
|
||||
def test_api_no_document_type(self):
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_document_type",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_api_add_invalid_tag(self):
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
@ -946,6 +962,14 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
|
||||
def test_api_add_tag_no_tag(self):
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "add_tag",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_api_delete_invalid_tag(self):
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
@ -957,6 +981,14 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
|
||||
def test_api_delete_tag_no_tag(self):
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "remove_tag",
|
||||
"parameters": {}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_api_modify_invalid_tags(self):
|
||||
self.assertEqual(list(self.doc2.tags.all()), [self.t1])
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
@ -966,6 +998,21 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_api_modify_tags_no_tags(self):
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {"remove_tags": [1123123]}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
response = self.client.post("/api/documents/bulk_edit/", json.dumps({
|
||||
"documents": [self.doc2.id],
|
||||
"method": "modify_tags",
|
||||
"parameters": {'add_tags': [self.t2.id, 1657]}
|
||||
}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_api_selection_data_empty(self):
|
||||
response = self.client.post("/api/documents/selection_data/", json.dumps({
|
||||
"documents": []
|
||||
|
@ -7,9 +7,8 @@ from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command, CommandError
|
||||
from django.test import override_settings, TransactionTestCase, TestCase
|
||||
from django.test import override_settings, TransactionTestCase
|
||||
|
||||
from documents.management.commands.document_consumer import _test_inotify
|
||||
from documents.models import Tag
|
||||
from documents.consumer import ConsumerError
|
||||
from documents.management.commands import document_consumer
|
||||
@ -261,27 +260,3 @@ class TestConsumerTags(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
|
||||
@override_settings(CONSUMER_POLLING=1)
|
||||
def test_consume_file_with_path_tags_polling(self):
|
||||
self.test_consume_file_with_path_tags()
|
||||
|
||||
|
||||
class TestInotify(DirectoriesMixin, TestCase):
|
||||
|
||||
def test_inotify(self):
|
||||
self.assertTrue(_test_inotify(self.dirs.consumption_dir))
|
||||
|
||||
@mock.patch("documents.management.commands.document_consumer.Path.touch")
|
||||
def test_inotify_error(self, m):
|
||||
m.side_effect = OSError("Permission error")
|
||||
self.assertFalse(_test_inotify(self.dirs.consumption_dir))
|
||||
|
||||
@mock.patch("documents.management.commands.document_consumer.Command.handle_polling")
|
||||
@mock.patch("documents.management.commands.document_consumer.Command.handle_inotify")
|
||||
@mock.patch("documents.management.commands.document_consumer._test_inotify")
|
||||
def test_polling_fallback(self, test_inotify, handle_inotify, handle_polling):
|
||||
test_inotify.return_value = False
|
||||
|
||||
cmd = document_consumer.Command()
|
||||
cmd.handle(directory=settings.CONSUMPTION_DIR, oneshot=False)
|
||||
|
||||
test_inotify.assert_called_once()
|
||||
handle_polling.assert_called_once()
|
||||
handle_inotify.assert_not_called()
|
||||
|
@ -120,3 +120,4 @@ class TestParserAvailability(TestCase):
|
||||
|
||||
self.assertTrue(is_file_ext_supported('.pdf'))
|
||||
self.assertFalse(is_file_ext_supported('.hsdfh'))
|
||||
self.assertFalse(is_file_ext_supported(''))
|
||||
|
Loading…
x
Reference in New Issue
Block a user