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>
<td> <td>
<a <a
routerLink="/explorer/{{ d.id }}" routerLink="/explorer?spid={{ d.id }}"
title="Edit document" title="Edit document"
i18n-title i18n-title
style="overflow-wrap: anywhere" style="overflow-wrap: anywhere"
@ -386,20 +386,6 @@
(click)="clickTag(t.id); $event.stopPropagation()" (click)="clickTag(t.id); $event.stopPropagation()"
></app-tag> ></app-tag>
</td> </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"> <td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type"> <ng-container *ngIf="d.document_type">
<a <a

View File

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

View File

@ -1,10 +1,7 @@
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core' 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 { 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 { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { Results } from 'src/app/data/results' import { Results } from 'src/app/data/results'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params' import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
@ -26,8 +23,6 @@ interface SelectionData {
providedIn: 'root', providedIn: 'root',
}) })
export class CustomStoragePathService extends AbstractPaperlessService<PaperlessStoragePath> { export class CustomStoragePathService extends AbstractPaperlessService<PaperlessStoragePath> {
private _searchQuery: string
constructor(http: HttpClient) { constructor(http: HttpClient) {
super(http, 'storage_paths') super(http, 'storage_paths')
} }
@ -38,16 +33,40 @@ export class CustomStoragePathService extends AbstractPaperlessService<Paperless
sortField?: string, sortField?: string,
sortReverse?: boolean, sortReverse?: boolean,
filterRules?: FilterRule[], filterRules?: FilterRule[],
extraParams = {} extraParams = {},
parentStoragePathId?: number
): Observable<Results<PaperlessStoragePath>> { ): Observable<Results<PaperlessStoragePath>> {
return this.list( const params = Object.assign(
page, extraParams,
pageSize, queryParamsFromFilterRules(filterRules)
sortField, )
sortReverse, if (parentStoragePathId !== null && parentStoragePathId !== undefined) {
Object.assign(extraParams, queryParamsFromFilterRules(filterRules)) return this.get(parentStoragePathId).pipe(
).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) => { map((results) => {
results.results = results.results.filter(
(s) => s.path.split('/').length === 1
)
return results return results
}) })
) )
@ -58,79 +77,4 @@ export class CustomStoragePathService extends AbstractPaperlessService<Paperless
fields: 'id', fields: 'id',
}).pipe(map((response) => response.results.map((doc) => doc.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. * Contains the IDs of all selected documents.
*/ */
selected?: Set<number> selected?: Set<number>
storagePathId?: number | null
} }
/** /**
@ -108,6 +110,7 @@ export class StoragePathListViewService {
sortReverse: true, sortReverse: true,
filterRules: [], filterRules: [],
selected: new Set<number>(), selected: new Set<number>(),
storagePathId: null,
} }
} }
@ -121,56 +124,22 @@ export class StoragePathListViewService {
return this.listViewStates.get(this._activeSavedViewId) 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) { loadFromQueryParams(queryParams: ParamMap) {
const paramsEmpty: boolean = queryParams.keys.length == 0 const isParamsEmpty: boolean = queryParams.keys.length == 0
let newState: ListViewState = this.listViewStates.get( let newState: ListViewState & { storagePathId?: number } =
this._activeSavedViewId this.listViewStates.get(this._activeSavedViewId)
) if (!isParamsEmpty) {
if (!paramsEmpty) newState = paramsToViewState(queryParams) newState = paramsToViewState(queryParams)
if (queryParams.has('spid')) {
newState.storagePathId = parseInt(queryParams.get('spid'))
}
}
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
// only reload if things have changed // only reload if things have changed
if ( if (
!this.initialized || !this.initialized ||
paramsEmpty || isParamsEmpty ||
this.activeListViewState.sortField !== newState.sortField || this.activeListViewState.sortField !== newState.sortField ||
this.activeListViewState.sortReverse !== newState.sortReverse || this.activeListViewState.sortReverse !== newState.sortReverse ||
this.activeListViewState.currentPage !== newState.currentPage || this.activeListViewState.currentPage !== newState.currentPage ||
@ -183,7 +152,8 @@ export class StoragePathListViewService {
this.activeListViewState.sortField = newState.sortField this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage 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.sortField,
activeListViewState.sortReverse, activeListViewState.sortReverse,
activeListViewState.filterRules, activeListViewState.filterRules,
{ truncate_content: true } { truncate_content: true },
activeListViewState.storagePathId
) )
.subscribe({ .subscribe({
next: (result) => { next: (result) => {
console.log('list filtered result:', result) console.log('result:', result)
this.initialized = true this.initialized = true
this.isReloading = false this.isReloading = false
activeListViewState.collectionSize = result.count activeListViewState.collectionSize = result.count

View File

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