Get storage path explorer somewhat working

This commit is contained in:
Martin Tan 2023-05-29 03:10:37 +08:00
parent 5ae5777467
commit 10ab272df7
5 changed files with 229 additions and 360 deletions

View File

@ -370,7 +370,7 @@
</td>
<td>
<a
routerLink="/explorer/{{ d.id }}"
routerLink="/explorer?spid={{ d.id }}"
title="Edit document"
i18n-title
style="overflow-wrap: anywhere"
@ -386,20 +386,6 @@
(click)="clickTag(t.id); $event.stopPropagation()"
></app-tag>
</td>
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell">
<a
*ngIf="d.notes.length"
routerLink="/documents/{{ d.id }}/notes"
class="btn btn-sm p-0"
>
<span class="badge rounded-pill bg-light border text-primary">
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text" />
</svg>
{{ d.notes.length }}</span
>
</a>
</td>
<td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type">
<a

View File

@ -8,7 +8,16 @@ import {
} from '@angular/core'
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, filter, first, map, switchMap, takeUntil } from 'rxjs'
import {
Subject,
filter,
first,
map,
switchMap,
take,
takeUntil,
tap,
} from 'rxjs'
import {
FilterRule,
filterRulesDiffer,
@ -34,6 +43,7 @@ import { StoragePathListViewService } from 'src/app/services/storage-path-list-v
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
@Component({
selector: 'app-explorer',
@ -140,44 +150,11 @@ export class ExplorerComponent
this.list.reload()
})
this.route.paramMap
.pipe(
filter((params) => params.has('id')), // only on saved view e.g. /view/id
switchMap((params) => {
return this.savedViewService
.getCached(+params.get('id'))
.pipe(map((view) => ({ view })))
})
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ view }) => {
if (!view) {
this.router.navigate(['404'])
return
}
this.unmodifiedSavedView = view
this.list.activateSavedViewWithQueryParams(
view,
convertToParamMap(this.route.snapshot.queryParams)
)
// this.list.reload()
this.unmodifiedFilterRules = view.filter_rules
})
this.route.queryParamMap
.pipe(
filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on /view/id
takeUntil(this.unsubscribeNotifier)
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((queryParams) => {
if (queryParams.has('view')) {
// loading a saved view on /documents
this.loadViewConfig(parseInt(queryParams.get('view')))
} else {
this.list.activateSavedView(null)
this.list.loadFromQueryParams(queryParams)
this.unmodifiedFilterRules = []
}
this.list.loadFromQueryParams(queryParams)
this.unmodifiedFilterRules = []
})
}
@ -187,19 +164,10 @@ export class ExplorerComponent
this.unsubscribeNotifier.complete()
}
loadViewConfig(viewID: number) {
this.savedViewService
.getCached(viewID)
.pipe(first())
.subscribe((view) => {
this.unmodifiedSavedView = view
this.list.activateSavedView(view)
this.list.reload()
})
}
openDocumentDetail(document: PaperlessDocument) {
this.router.navigate(['documents', document.id])
openDocumentDetail(storagePath: PaperlessStoragePath) {
this.router.navigate(['explorer'], {
queryParams: { spid: storagePath.id },
})
}
toggleSelected(document: PaperlessDocument, event: MouseEvent): void {

View File

@ -1,10 +1,7 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable, map } from 'rxjs'
import { Observable, filter, map, switchMap, tap } from 'rxjs'
import { FilterRule } from 'src/app/data/filter-rule'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { Results } from 'src/app/data/results'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
@ -26,8 +23,6 @@ interface SelectionData {
providedIn: 'root',
})
export class CustomStoragePathService extends AbstractPaperlessService<PaperlessStoragePath> {
private _searchQuery: string
constructor(http: HttpClient) {
super(http, 'storage_paths')
}
@ -38,16 +33,40 @@ export class CustomStoragePathService extends AbstractPaperlessService<Paperless
sortField?: string,
sortReverse?: boolean,
filterRules?: FilterRule[],
extraParams = {}
extraParams = {},
parentStoragePathId?: number
): Observable<Results<PaperlessStoragePath>> {
return this.list(
page,
pageSize,
sortField,
sortReverse,
Object.assign(extraParams, queryParamsFromFilterRules(filterRules))
).pipe(
const params = Object.assign(
extraParams,
queryParamsFromFilterRules(filterRules)
)
if (parentStoragePathId !== null && parentStoragePathId !== undefined) {
return this.get(parentStoragePathId).pipe(
switchMap((storagePath) => {
params.path__istartswith = storagePath.path
return this.list(page, pageSize, sortField, sortReverse, params).pipe(
map((results) => {
results.results = results.results.filter((s) => {
const isNotParent = s.id !== parentStoragePathId
const isDirectChild =
s.path
.replace(storagePath.path, '')
.split('/')
.filter((s) => !!s).length === 1
return isNotParent && isDirectChild
})
return results
})
)
})
)
}
return this.list(page, pageSize, sortField, sortReverse, params).pipe(
map((results) => {
results.results = results.results.filter(
(s) => s.path.split('/').length === 1
)
return results
})
)
@ -58,79 +77,4 @@ export class CustomStoragePathService extends AbstractPaperlessService<Paperless
fields: 'id',
}).pipe(map((response) => response.results.map((doc) => doc.id)))
}
getPreviewUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'preview')
if (this._searchQuery) url += `#search="${this._searchQuery}"`
if (original) {
url += '?original=true'
}
return url
}
getThumbUrl(id: number): string {
return this.getResourceUrl(id, 'thumb')
}
getDownloadUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'download')
if (original) {
url += '?original=true'
}
return url
}
update(o: PaperlessDocument): Observable<PaperlessDocument> {
// we want to only set created_date
o.created = undefined
return super.update(o)
}
uploadDocument(formData) {
return this.http.post(
this.getResourceUrl(null, 'post_document'),
formData,
{ reportProgress: true, observe: 'events' }
)
}
getMetadata(id: number): Observable<PaperlessDocumentMetadata> {
return this.http.get<PaperlessDocumentMetadata>(
this.getResourceUrl(id, 'metadata')
)
}
bulkEdit(ids: number[], method: string, args: any) {
return this.http.post(this.getResourceUrl(null, 'bulk_edit'), {
documents: ids,
method: method,
parameters: args,
})
}
getSuggestions(id: number): Observable<PaperlessDocumentSuggestions> {
return this.http.get<PaperlessDocumentSuggestions>(
this.getResourceUrl(id, 'suggestions')
)
}
bulkDownload(
ids: number[],
content = 'both',
useFilenameFormatting: boolean = false
) {
return this.http.post(
this.getResourceUrl(null, 'bulk_download'),
{
documents: ids,
content: content,
follow_formatting: useFilenameFormatting,
},
{ responseType: 'blob' }
)
}
public set searchQuery(query: string) {
this._searchQuery = query
}
}

View File

@ -57,6 +57,8 @@ export interface ListViewState {
* Contains the IDs of all selected documents.
*/
selected?: Set<number>
storagePathId?: number | null
}
/**
@ -108,6 +110,7 @@ export class StoragePathListViewService {
sortReverse: true,
filterRules: [],
selected: new Set<number>(),
storagePathId: null,
}
}
@ -121,56 +124,22 @@ export class StoragePathListViewService {
return this.listViewStates.get(this._activeSavedViewId)
}
activateSavedView(view: PaperlessSavedView) {
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
if (view) {
this._activeSavedViewId = view.id
this.loadSavedView(view)
} else {
this._activeSavedViewId = null
}
}
activateSavedViewWithQueryParams(
view: PaperlessSavedView,
queryParams: ParamMap
) {
const viewState = paramsToViewState(queryParams)
this.activateSavedView(view)
this.currentPage = viewState.currentPage
}
loadSavedView(view: PaperlessSavedView, closeCurrentView: boolean = false) {
if (closeCurrentView) {
this._activeSavedViewId = null
}
this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
this.activeListViewState.sortField = view.sort_field
this.activeListViewState.sortReverse = view.sort_reverse
if (this._activeSavedViewId) {
this.activeListViewState.title = view.name
}
this.reduceSelectionToFilter()
if (!this.router.routerState.snapshot.url.includes('/view/')) {
this.router.navigate(['view', view.id])
}
}
loadFromQueryParams(queryParams: ParamMap) {
const paramsEmpty: boolean = queryParams.keys.length == 0
let newState: ListViewState = this.listViewStates.get(
this._activeSavedViewId
)
if (!paramsEmpty) newState = paramsToViewState(queryParams)
const isParamsEmpty: boolean = queryParams.keys.length == 0
let newState: ListViewState & { storagePathId?: number } =
this.listViewStates.get(this._activeSavedViewId)
if (!isParamsEmpty) {
newState = paramsToViewState(queryParams)
if (queryParams.has('spid')) {
newState.storagePathId = parseInt(queryParams.get('spid'))
}
}
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
// only reload if things have changed
if (
!this.initialized ||
paramsEmpty ||
isParamsEmpty ||
this.activeListViewState.sortField !== newState.sortField ||
this.activeListViewState.sortReverse !== newState.sortReverse ||
this.activeListViewState.currentPage !== newState.currentPage ||
@ -183,7 +152,8 @@ export class StoragePathListViewService {
this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage
this.reload(null, paramsEmpty) // update the params if there arent any
this.activeListViewState.storagePathId = newState.storagePathId
this.reload(null, isParamsEmpty) // update the params if there arent any
}
}
@ -198,11 +168,12 @@ export class StoragePathListViewService {
activeListViewState.sortField,
activeListViewState.sortReverse,
activeListViewState.filterRules,
{ truncate_content: true }
{ truncate_content: true },
activeListViewState.storagePathId
)
.subscribe({
next: (result) => {
console.log('list filtered result:', result)
console.log('result:', result)
this.initialized = true
this.isReloading = false
activeListViewState.collectionSize = result.count

View File

@ -1,159 +1,159 @@
from django.db.models import Q
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import Filter
from django_filters.rest_framework import FilterSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from .models import Correspondent
from .models import Document
from .models import DocumentType
from .models import Log
from .models import StoragePath
from .models import Tag
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
ID_KWARGS = ["in", "exact"]
INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"]
class CorrespondentFilterSet(FilterSet):
class Meta:
model = Correspondent
fields = {"name": CHAR_KWARGS}
class TagFilterSet(FilterSet):
class Meta:
model = Tag
fields = {"name": CHAR_KWARGS}
class DocumentTypeFilterSet(FilterSet):
class Meta:
model = DocumentType
fields = {"name": CHAR_KWARGS}
class ObjectFilter(Filter):
def __init__(self, exclude=False, in_list=False, field_name=""):
super().__init__()
self.exclude = exclude
self.in_list = in_list
self.field_name = field_name
def filter(self, qs, value):
if not value:
return qs
try:
object_ids = [int(x) for x in value.split(",")]
except ValueError:
return qs
if self.in_list:
qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
else:
for obj_id in object_ids:
if self.exclude:
qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
else:
qs = qs.filter(**{f"{self.field_name}__id": obj_id})
return qs
class InboxFilter(Filter):
def filter(self, qs, value):
if value == "true":
return qs.filter(tags__is_inbox_tag=True)
elif value == "false":
return qs.exclude(tags__is_inbox_tag=True)
else:
return qs
class TitleContentFilter(Filter):
def filter(self, qs, value):
if value:
return qs.filter(Q(title__icontains=value) | Q(content__icontains=value))
else:
return qs
class DocumentFilterSet(FilterSet):
is_tagged = BooleanFilter(
label="Is tagged",
field_name="tags",
lookup_expr="isnull",
exclude=True,
)
tags__id__all = ObjectFilter(field_name="tags")
tags__id__none = ObjectFilter(field_name="tags", exclude=True)
tags__id__in = ObjectFilter(field_name="tags", in_list=True)
correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
is_in_inbox = InboxFilter()
title_content = TitleContentFilter()
class Meta:
model = Document
fields = {
"title": CHAR_KWARGS,
"content": CHAR_KWARGS,
"archive_serial_number": INT_KWARGS,
"created": DATE_KWARGS,
"added": DATE_KWARGS,
"modified": DATE_KWARGS,
"correspondent": ["isnull"],
"correspondent__id": ID_KWARGS,
"correspondent__name": CHAR_KWARGS,
"tags__id": ID_KWARGS,
"tags__name": CHAR_KWARGS,
"document_type": ["isnull"],
"document_type__id": ID_KWARGS,
"document_type__name": CHAR_KWARGS,
"storage_path": ["isnull"],
"storage_path__id": ID_KWARGS,
"storage_path__name": CHAR_KWARGS,
}
class LogFilterSet(FilterSet):
class Meta:
model = Log
fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS}
class StoragePathFilterSet(FilterSet):
class Meta:
model = StoragePath
fields = {
"name": CHAR_KWARGS,
"path": CHAR_KWARGS,
}
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
"""
A filter backend that limits results to those where the requesting user
has read object level permissions, owns the objects, or objects without
an owner (for backwards compat)
"""
def filter_queryset(self, request, queryset, view):
objects_with_perms = super().filter_queryset(request, queryset, view)
objects_owned = queryset.filter(owner=request.user)
objects_unowned = queryset.filter(owner__isnull=True)
return objects_with_perms | objects_owned | objects_unowned
from django.db.models import Q
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import Filter
from django_filters.rest_framework import FilterSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from .models import Correspondent
from .models import Document
from .models import DocumentType
from .models import Log
from .models import StoragePath
from .models import Tag
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
ID_KWARGS = ["in", "exact"]
INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"]
class CorrespondentFilterSet(FilterSet):
class Meta:
model = Correspondent
fields = {"name": CHAR_KWARGS}
class TagFilterSet(FilterSet):
class Meta:
model = Tag
fields = {"name": CHAR_KWARGS}
class DocumentTypeFilterSet(FilterSet):
class Meta:
model = DocumentType
fields = {"name": CHAR_KWARGS}
class ObjectFilter(Filter):
def __init__(self, exclude=False, in_list=False, field_name=""):
super().__init__()
self.exclude = exclude
self.in_list = in_list
self.field_name = field_name
def filter(self, qs, value):
if not value:
return qs
try:
object_ids = [int(x) for x in value.split(",")]
except ValueError:
return qs
if self.in_list:
qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
else:
for obj_id in object_ids:
if self.exclude:
qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
else:
qs = qs.filter(**{f"{self.field_name}__id": obj_id})
return qs
class InboxFilter(Filter):
def filter(self, qs, value):
if value == "true":
return qs.filter(tags__is_inbox_tag=True)
elif value == "false":
return qs.exclude(tags__is_inbox_tag=True)
else:
return qs
class TitleContentFilter(Filter):
def filter(self, qs, value):
if value:
return qs.filter(Q(title__icontains=value) | Q(content__icontains=value))
else:
return qs
class DocumentFilterSet(FilterSet):
is_tagged = BooleanFilter(
label="Is tagged",
field_name="tags",
lookup_expr="isnull",
exclude=True,
)
tags__id__all = ObjectFilter(field_name="tags")
tags__id__none = ObjectFilter(field_name="tags", exclude=True)
tags__id__in = ObjectFilter(field_name="tags", in_list=True)
correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
is_in_inbox = InboxFilter()
title_content = TitleContentFilter()
class Meta:
model = Document
fields = {
"title": CHAR_KWARGS,
"content": CHAR_KWARGS,
"archive_serial_number": INT_KWARGS,
"created": DATE_KWARGS,
"added": DATE_KWARGS,
"modified": DATE_KWARGS,
"correspondent": ["isnull"],
"correspondent__id": ID_KWARGS,
"correspondent__name": CHAR_KWARGS,
"tags__id": ID_KWARGS,
"tags__name": CHAR_KWARGS,
"document_type": ["isnull"],
"document_type__id": ID_KWARGS,
"document_type__name": CHAR_KWARGS,
"storage_path": ["isnull"],
"storage_path__id": ID_KWARGS,
"storage_path__name": CHAR_KWARGS,
}
class LogFilterSet(FilterSet):
class Meta:
model = Log
fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS}
class StoragePathFilterSet(FilterSet):
class Meta:
model = StoragePath
fields = {
"name": CHAR_KWARGS,
"path": CHAR_KWARGS,
}
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
"""
A filter backend that limits results to those where the requesting user
has read object level permissions, owns the objects, or objects without
an owner (for backwards compat)
"""
def filter_queryset(self, request, queryset, view):
objects_with_perms = super().filter_queryset(request, queryset, view)
objects_owned = queryset.filter(owner=request.user)
objects_unowned = queryset.filter(owner__isnull=True)
return objects_with_perms | objects_owned | objects_unowned