From c0f799a807a4e5780b3fd495e08895c4003be44b Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 6 May 2022 19:49:45 -0700 Subject: [PATCH 01/24] Update settings.service.ts --- src-ui/src/app/services/settings.service.ts | 42 +++++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 1a424e4ca..802c55611 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -65,18 +65,46 @@ const SETTINGS: PaperlessSettings[] = [ type: 'boolean', default: false, }, - { key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: 'number', default: 50 }, - { key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: 'boolean', default: true }, - { key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: 'boolean', default: false }, + { + key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, + type: 'number', + default: 50, + }, + { + key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.DARK_MODE_ENABLED, + type: 'boolean', + default: false, + }, { key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, type: 'boolean', default: true, }, - { key: SETTINGS_KEYS.THEME_COLOR, type: 'string', default: '' }, - { key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: 'boolean', default: false }, - { key: SETTINGS_KEYS.DATE_LOCALE, type: 'string', default: '' }, - { key: SETTINGS_KEYS.DATE_FORMAT, type: 'string', default: 'mediumDate' }, + { + key: SETTINGS_KEYS.THEME_COLOR, + type: 'string', + default: '', + }, + { + key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, + type: 'boolean', + default: false, + }, + { + key: SETTINGS_KEYS.DATE_LOCALE, + type: 'string', + default: '', + }, + { + key: SETTINGS_KEYS.DATE_FORMAT, + type: 'string', + default: 'mediumDate', + }, { key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT, type: 'boolean', From 8f2687e39048cdd84eeaab8a9460ae2caa0031ac Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 6 May 2022 22:10:35 -0700 Subject: [PATCH 02/24] frontend settings db migration --- .../migrations/1019_frontendsettings.py | 39 +++++++++++++++++++ src/documents/models.py | 22 +++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/documents/migrations/1019_frontendsettings.py diff --git a/src/documents/migrations/1019_frontendsettings.py b/src/documents/migrations/1019_frontendsettings.py new file mode 100644 index 000000000..1da17c974 --- /dev/null +++ b/src/documents/migrations/1019_frontendsettings.py @@ -0,0 +1,39 @@ +# Generated by Django 4.0.4 on 2022-05-07 05:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("documents", "1018_alter_savedviewfilterrule_value"), + ] + + operations = [ + migrations.CreateModel( + name="FrontendSettings", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("settings", models.JSONField(null=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="frontend_settings", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 206df4e8a..198bf27b2 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -9,6 +9,8 @@ import pathvalidate from django.conf import settings from django.contrib.auth.models import User from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver from django.utils import timezone from django.utils.translation import gettext_lazy as _ from documents.parsers import get_default_file_extension @@ -465,3 +467,23 @@ class FileInfo: cls._mangle_property(properties, "created") cls._mangle_property(properties, "title") return cls(**properties) + + +# Extending User Model Using a One-To-One Link +class FrontendSettings(models.Model): + + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + related_name="frontend_settings", + ) + settings = models.JSONField(null=True) + + def __str__(self): + return self.user.username + + +@receiver(post_save, sender=User) +def create_user_frontend_settings(sender, instance, created, **kwargs): + if created: + FrontendSettings.objects.create(user=instance) From 4b37c1963b34cb9d92f9ada7bd785e1f495558c1 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 6 May 2022 22:29:30 -0700 Subject: [PATCH 03/24] skeleton frontend_settings api endpoint --- src/documents/serialisers.py | 19 +++++++++++++++++++ src/documents/views.py | 35 +++++++++++++++++++++++++++++++++++ src/paperless/urls.py | 6 ++++++ 3 files changed, 60 insertions(+) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index a257a1dd6..0339db88e 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -11,6 +11,7 @@ from . import bulk_edit from .models import Correspondent from .models import Document from .models import DocumentType +from .models import FrontendSettings from .models import MatchingModel from .models import SavedView from .models import SavedViewFilterRule @@ -498,3 +499,21 @@ class BulkDownloadSerializer(DocumentListSerializer): "bzip2": zipfile.ZIP_BZIP2, "lzma": zipfile.ZIP_LZMA, }[compression] + + +class FrontendSettingsViewSerializer(serializers.ModelSerializer): + class Meta: + model = FrontendSettings + depth = 1 + fields = [ + "id", + "settings", + ] + + def update(self, instance, validated_data): + super(FrontendSettingsViewSerializer, self).update(instance, validated_data) + return instance + + def create(self, validated_data): + frontend_settings = FrontendSettings.objects.create(**validated_data) + return frontend_settings diff --git a/src/documents/views.py b/src/documents/views.py index 5d179da02..8b548eed7 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -11,6 +11,7 @@ from unicodedata import normalize from urllib.parse import quote from django.conf import settings +from django.contrib.auth.models import User from django.db.models import Case from django.db.models import Count from django.db.models import IntegerField @@ -70,6 +71,7 @@ from .serialisers import CorrespondentSerializer from .serialisers import DocumentListSerializer from .serialisers import DocumentSerializer from .serialisers import DocumentTypeSerializer +from .serialisers import FrontendSettingsViewSerializer from .serialisers import PostDocumentSerializer from .serialisers import SavedViewSerializer from .serialisers import TagSerializer @@ -715,3 +717,36 @@ class RemoteVersionView(GenericAPIView): "feature_is_set": feature_is_set, }, ) + + +class FrontendSettingsView(GenericAPIView): + + permission_classes = (IsAuthenticated,) + serializer_class = FrontendSettingsViewSerializer + + def get(self, request, format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user = User.objects.get(pk=request.user.id) + settings = [] + if hasattr(user, "frontend_settings"): + settings = user.frontend_settings.settings + return Response( + { + "user_id": user.id, + "settings": settings, + }, + ) + + def post(self, request, format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + serializer.save(user=self.request.user) + + return Response( + { + "success": True, + }, + ) diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 833788cce..0ad9d072f 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -11,6 +11,7 @@ from documents.views import BulkDownloadView from documents.views import BulkEditView from documents.views import CorrespondentViewSet from documents.views import DocumentTypeViewSet +from documents.views import FrontendSettingsView from documents.views import IndexView from documents.views import LogViewSet from documents.views import PostDocumentView @@ -78,6 +79,11 @@ urlpatterns = [ RemoteVersionView.as_view(), name="remoteversion", ), + re_path( + r"^frontend_settings/", + FrontendSettingsView.as_view(), + name="frontend_settings", + ), path("token/", views.obtain_auth_token), ] + api_router.urls, From a697eb8530552b277b9c6b2770c460fff35a260f Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 00:05:51 -0700 Subject: [PATCH 04/24] Basic frontend settings retrieval --- .../manage/settings/settings.component.ts | 179 ++++++++++-------- src-ui/src/app/services/settings.service.ts | 52 ++++- src/documents/serialisers.py | 2 +- 3 files changed, 146 insertions(+), 87 deletions(-) diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 45785c709..0978a94ab 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -17,7 +17,7 @@ import { } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' -import { Observable, Subscription, BehaviorSubject } from 'rxjs' +import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' @Component({ selector: 'app-settings', @@ -72,85 +72,93 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { ngOnInit() { this.savedViewService.listAll().subscribe((r) => { this.savedViews = r.results - let storeData = { - bulkEditConfirmationDialogs: this.settings.get( - SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS - ), - bulkEditApplyOnClose: this.settings.get( - SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE - ), - documentListItemPerPage: this.settings.get( - SETTINGS_KEYS.DOCUMENT_LIST_SIZE - ), - darkModeUseSystem: this.settings.get( - SETTINGS_KEYS.DARK_MODE_USE_SYSTEM - ), - darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED), - darkModeInvertThumbs: this.settings.get( - SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED - ), - themeColor: this.settings.get(SETTINGS_KEYS.THEME_COLOR), - useNativePdfViewer: this.settings.get( - SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER - ), - savedViews: {}, - displayLanguage: this.settings.getLanguage(), - dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), - dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), - notificationsConsumerNewDocument: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT - ), - notificationsConsumerSuccess: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS - ), - notificationsConsumerFailed: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED - ), - notificationsConsumerSuppressOnDashboard: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD - ), - } + this.settings + .retrieveSettings() + .pipe(first()) + .subscribe(() => { + let storeData = { + bulkEditConfirmationDialogs: this.settings.get( + SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS + ), + bulkEditApplyOnClose: this.settings.get( + SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE + ), + documentListItemPerPage: this.settings.get( + SETTINGS_KEYS.DOCUMENT_LIST_SIZE + ), + darkModeUseSystem: this.settings.get( + SETTINGS_KEYS.DARK_MODE_USE_SYSTEM + ), + darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED), + darkModeInvertThumbs: this.settings.get( + SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED + ), + themeColor: this.settings.get(SETTINGS_KEYS.THEME_COLOR), + useNativePdfViewer: this.settings.get( + SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER + ), + savedViews: {}, + displayLanguage: this.settings.getLanguage(), + dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), + dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), + notificationsConsumerNewDocument: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT + ), + notificationsConsumerSuccess: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS + ), + notificationsConsumerFailed: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED + ), + notificationsConsumerSuppressOnDashboard: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD + ), + } - for (let view of this.savedViews) { - storeData.savedViews[view.id.toString()] = { - id: view.id, - name: view.name, - show_on_dashboard: view.show_on_dashboard, - show_in_sidebar: view.show_in_sidebar, - } - this.savedViewGroup.addControl( - view.id.toString(), - new FormGroup({ - id: new FormControl(null), - name: new FormControl(null), - show_on_dashboard: new FormControl(null), - show_in_sidebar: new FormControl(null), + for (let view of this.savedViews) { + storeData.savedViews[view.id.toString()] = { + id: view.id, + name: view.name, + show_on_dashboard: view.show_on_dashboard, + show_in_sidebar: view.show_in_sidebar, + } + this.savedViewGroup.addControl( + view.id.toString(), + new FormGroup({ + id: new FormControl(null), + name: new FormControl(null), + show_on_dashboard: new FormControl(null), + show_in_sidebar: new FormControl(null), + }) + ) + } + + this.store = new BehaviorSubject(storeData) + + this.storeSub = this.store.asObservable().subscribe((state) => { + this.settingsForm.patchValue(state, { emitEvent: false }) }) - ) - } - this.store = new BehaviorSubject(storeData) + // Initialize dirtyCheck + this.isDirty$ = dirtyCheck( + this.settingsForm, + this.store.asObservable() + ) - this.storeSub = this.store.asObservable().subscribe((state) => { - this.settingsForm.patchValue(state, { emitEvent: false }) - }) + // Record dirty in case we need to 'undo' appearance settings if not saved on close + this.isDirty$.subscribe((dirty) => { + this.isDirty = dirty + }) - // Initialize dirtyCheck - this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable()) - - // Record dirty in case we need to 'undo' appearance settings if not saved on close - this.isDirty$.subscribe((dirty) => { - this.isDirty = dirty - }) - - // "Live" visual changes prior to save - this.settingsForm.valueChanges.subscribe(() => { - this.settings.updateAppearanceSettings( - this.settingsForm.get('darkModeUseSystem').value, - this.settingsForm.get('darkModeEnabled').value, - this.settingsForm.get('themeColor').value - ) - }) + // "Live" visual changes prior to save + this.settingsForm.valueChanges.subscribe(() => { + this.settings.updateAppearanceSettings( + this.settingsForm.get('darkModeUseSystem').value, + this.settingsForm.get('darkModeEnabled').value, + this.settingsForm.get('themeColor').value + ) + }) + }) }) } @@ -227,10 +235,23 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { this.settingsForm.value.notificationsConsumerSuppressOnDashboard ) this.settings.setLanguage(this.settingsForm.value.displayLanguage) - this.store.next(this.settingsForm.value) - this.documentListViewService.updatePageSize() - this.settings.updateAppearanceSettings() - this.toastService.showInfo($localize`Settings saved successfully.`) + this.settings + .storeSettings() + .pipe(first()) + .subscribe({ + next: () => { + this.store.next(this.settingsForm.value) + this.documentListViewService.updatePageSize() + this.settings.updateAppearanceSettings() + this.toastService.showInfo($localize`Settings saved successfully.`) + }, + error: (error) => { + this.toastService.showError( + $localize`An error occurred while saving settings.` + ) + console.log(error) + }, + }) } get displayLanguageOptions(): LanguageOption[] { diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 802c55611..f4708880a 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -1,4 +1,5 @@ import { DOCUMENT } from '@angular/common' +import { HttpClient } from '@angular/common/http' import { Inject, Injectable, @@ -9,11 +10,14 @@ import { } from '@angular/core' import { Meta } from '@angular/platform-browser' import { CookieService } from 'ngx-cookie-service' +import { first, Observable } from 'rxjs' import { BRIGHTNESS, estimateBrightnessForColor, hexToHsl, } from 'src/app/utils/color' +import { environment } from 'src/environments/environment' +import { Results } from '../data/results' export interface PaperlessSettings { key: string @@ -132,17 +136,34 @@ const SETTINGS: PaperlessSettings[] = [ }) export class SettingsService { private renderer: Renderer2 + protected baseUrl: string = environment.apiBaseUrl + 'frontend_settings/' + + private settings: Object = {} + private settings$: Observable> constructor( - private rendererFactory: RendererFactory2, + rendererFactory: RendererFactory2, @Inject(DOCUMENT) private document, private cookieService: CookieService, private meta: Meta, - @Inject(LOCALE_ID) private localeId: string + @Inject(LOCALE_ID) private localeId: string, + protected http: HttpClient ) { this.renderer = rendererFactory.createRenderer(null, null) - this.updateAppearanceSettings() + this.retrieveSettings() + .pipe(first()) + .subscribe((response) => { + Object.assign(this.settings, response['settings']) + + this.updateAppearanceSettings() + }) + } + + public retrieveSettings(): Observable> { + if (!this.settings$) + this.settings$ = this.http.get>(this.baseUrl) + return this.settings$ } public updateAppearanceSettings( @@ -390,7 +411,16 @@ export class SettingsService { return null } - let value = localStorage.getItem(key) + let value = null + // parse key:key:key into nested object + const keys = key.replace('general-settings:', '').split(':') + let settingObj = this.settings + keys.forEach((keyPart, index) => { + keyPart = keyPart.replace(/-/g, '_') + if (!settingObj.hasOwnProperty(keyPart)) return + if (index == keys.length - 1) value = settingObj[keyPart] + else settingObj = settingObj[keyPart] + }) if (value != null) { switch (setting.type) { @@ -409,10 +439,18 @@ export class SettingsService { } set(key: string, value: any) { - localStorage.setItem(key, value.toString()) + // parse key:key:key into nested object + let settingObj = this.settings + const keys = key.replace('general-settings:', '').split(':') + keys.forEach((keyPart, index) => { + keyPart = keyPart.replace(/-/g, '_') + if (!settingObj.hasOwnProperty(keyPart)) settingObj[keyPart] = {} + if (index == keys.length - 1) settingObj[keyPart] = value + else settingObj = settingObj[keyPart] + }) } - unset(key: string) { - localStorage.removeItem(key) + storeSettings(): Observable { + return this.http.post(this.baseUrl, { settings: this.settings }) } } diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 0339db88e..75b6b8cdd 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -515,5 +515,5 @@ class FrontendSettingsViewSerializer(serializers.ModelSerializer): return instance def create(self, validated_data): - frontend_settings = FrontendSettings.objects.create(**validated_data) + frontend_settings = FrontendSettings.objects.update_or_create(**validated_data) return frontend_settings From 321e0ced2ae6b3ac2af0223b11b76f033c3bc30a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 00:15:58 -0700 Subject: [PATCH 05/24] fix key constraint error --- src/documents/serialisers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 75b6b8cdd..9119779e8 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -515,5 +515,8 @@ class FrontendSettingsViewSerializer(serializers.ModelSerializer): return instance def create(self, validated_data): - frontend_settings = FrontendSettings.objects.update_or_create(**validated_data) + frontend_settings = FrontendSettings.objects.update_or_create( + user=validated_data.get("user"), + defaults={"settings": validated_data.get("settings", None)}, + ) return frontend_settings From 7d9575b7fd0f634161befbe70187b799fc80c48e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 00:41:35 -0700 Subject: [PATCH 06/24] Use app initializer to wait on settings --- src-ui/src/app/app.module.ts | 16 +- .../manage/settings/settings.component.ts | 156 +++++++++--------- src-ui/src/app/services/settings.service.ts | 20 +-- 3 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index e891d217d..79bbe9700 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule } from '@angular/platform-browser' -import { NgModule } from '@angular/core' +import { APP_INITIALIZER, NgModule } from '@angular/core' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' import { @@ -87,6 +87,8 @@ import localeSr from '@angular/common/locales/sr' import localeSv from '@angular/common/locales/sv' import localeTr from '@angular/common/locales/tr' import localeZh from '@angular/common/locales/zh' +import { Observable } from 'rxjs' +import { SettingsService } from './services/settings.service' registerLocaleData(localeBe) registerLocaleData(localeCs) @@ -109,6 +111,12 @@ registerLocaleData(localeSv) registerLocaleData(localeTr) registerLocaleData(localeZh) +function initializeApp(settings: SettingsService) { + return () => { + return settings.initializeSettings() + } +} + @NgModule({ declarations: [ AppComponent, @@ -174,6 +182,12 @@ registerLocaleData(localeZh) ColorSliderModule, ], providers: [ + { + provide: APP_INITIALIZER, + useFactory: initializeApp, + deps: [SettingsService], + multi: true, + }, DatePipe, CookieService, { diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 0978a94ab..ce999c14c 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -72,93 +72,85 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { ngOnInit() { this.savedViewService.listAll().subscribe((r) => { this.savedViews = r.results - this.settings - .retrieveSettings() - .pipe(first()) - .subscribe(() => { - let storeData = { - bulkEditConfirmationDialogs: this.settings.get( - SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS - ), - bulkEditApplyOnClose: this.settings.get( - SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE - ), - documentListItemPerPage: this.settings.get( - SETTINGS_KEYS.DOCUMENT_LIST_SIZE - ), - darkModeUseSystem: this.settings.get( - SETTINGS_KEYS.DARK_MODE_USE_SYSTEM - ), - darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED), - darkModeInvertThumbs: this.settings.get( - SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED - ), - themeColor: this.settings.get(SETTINGS_KEYS.THEME_COLOR), - useNativePdfViewer: this.settings.get( - SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER - ), - savedViews: {}, - displayLanguage: this.settings.getLanguage(), - dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), - dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), - notificationsConsumerNewDocument: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT - ), - notificationsConsumerSuccess: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS - ), - notificationsConsumerFailed: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED - ), - notificationsConsumerSuppressOnDashboard: this.settings.get( - SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD - ), - } + let storeData = { + bulkEditConfirmationDialogs: this.settings.get( + SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS + ), + bulkEditApplyOnClose: this.settings.get( + SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE + ), + documentListItemPerPage: this.settings.get( + SETTINGS_KEYS.DOCUMENT_LIST_SIZE + ), + darkModeUseSystem: this.settings.get( + SETTINGS_KEYS.DARK_MODE_USE_SYSTEM + ), + darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED), + darkModeInvertThumbs: this.settings.get( + SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED + ), + themeColor: this.settings.get(SETTINGS_KEYS.THEME_COLOR), + useNativePdfViewer: this.settings.get( + SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER + ), + savedViews: {}, + displayLanguage: this.settings.getLanguage(), + dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), + dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), + notificationsConsumerNewDocument: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT + ), + notificationsConsumerSuccess: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS + ), + notificationsConsumerFailed: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED + ), + notificationsConsumerSuppressOnDashboard: this.settings.get( + SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD + ), + } - for (let view of this.savedViews) { - storeData.savedViews[view.id.toString()] = { - id: view.id, - name: view.name, - show_on_dashboard: view.show_on_dashboard, - show_in_sidebar: view.show_in_sidebar, - } - this.savedViewGroup.addControl( - view.id.toString(), - new FormGroup({ - id: new FormControl(null), - name: new FormControl(null), - show_on_dashboard: new FormControl(null), - show_in_sidebar: new FormControl(null), - }) - ) - } - - this.store = new BehaviorSubject(storeData) - - this.storeSub = this.store.asObservable().subscribe((state) => { - this.settingsForm.patchValue(state, { emitEvent: false }) + for (let view of this.savedViews) { + storeData.savedViews[view.id.toString()] = { + id: view.id, + name: view.name, + show_on_dashboard: view.show_on_dashboard, + show_in_sidebar: view.show_in_sidebar, + } + this.savedViewGroup.addControl( + view.id.toString(), + new FormGroup({ + id: new FormControl(null), + name: new FormControl(null), + show_on_dashboard: new FormControl(null), + show_in_sidebar: new FormControl(null), }) + ) + } - // Initialize dirtyCheck - this.isDirty$ = dirtyCheck( - this.settingsForm, - this.store.asObservable() - ) + this.store = new BehaviorSubject(storeData) - // Record dirty in case we need to 'undo' appearance settings if not saved on close - this.isDirty$.subscribe((dirty) => { - this.isDirty = dirty - }) + this.storeSub = this.store.asObservable().subscribe((state) => { + this.settingsForm.patchValue(state, { emitEvent: false }) + }) - // "Live" visual changes prior to save - this.settingsForm.valueChanges.subscribe(() => { - this.settings.updateAppearanceSettings( - this.settingsForm.get('darkModeUseSystem').value, - this.settingsForm.get('darkModeEnabled').value, - this.settingsForm.get('themeColor').value - ) - }) - }) + // Initialize dirtyCheck + this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable()) + + // Record dirty in case we need to 'undo' appearance settings if not saved on close + this.isDirty$.subscribe((dirty) => { + this.isDirty = dirty + }) + + // "Live" visual changes prior to save + this.settingsForm.valueChanges.subscribe(() => { + this.settings.updateAppearanceSettings( + this.settingsForm.get('darkModeUseSystem').value, + this.settingsForm.get('darkModeEnabled').value, + this.settingsForm.get('themeColor').value + ) + }) }) } diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index f4708880a..f19afb956 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -139,7 +139,6 @@ export class SettingsService { protected baseUrl: string = environment.apiBaseUrl + 'frontend_settings/' private settings: Object = {} - private settings$: Observable> constructor( rendererFactory: RendererFactory2, @@ -150,20 +149,15 @@ export class SettingsService { protected http: HttpClient ) { this.renderer = rendererFactory.createRenderer(null, null) - - this.retrieveSettings() - .pipe(first()) - .subscribe((response) => { - Object.assign(this.settings, response['settings']) - - this.updateAppearanceSettings() - }) } - public retrieveSettings(): Observable> { - if (!this.settings$) - this.settings$ = this.http.get>(this.baseUrl) - return this.settings$ + // this is called by the app initializer in app.module + public initializeSettings(): Observable> { + let settings$ = this.http.get>(this.baseUrl) + settings$.pipe(first()).subscribe((response) => { + Object.assign(this.settings, response['settings']) + }) + return settings$ } public updateAppearanceSettings( From 2e97c0a5fbc7f964cdc230229064f3f5b2d53e2a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 01:53:37 -0700 Subject: [PATCH 07/24] Automatic migration to database --- src-ui/src/app/services/settings.service.ts | 40 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index f19afb956..f624f953d 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -18,6 +18,7 @@ import { } from 'src/app/utils/color' import { environment } from 'src/environments/environment' import { Results } from '../data/results' +import { ToastService } from './toast.service' export interface PaperlessSettings { key: string @@ -146,7 +147,8 @@ export class SettingsService { private cookieService: CookieService, private meta: Meta, @Inject(LOCALE_ID) private localeId: string, - protected http: HttpClient + protected http: HttpClient, + private toastService: ToastService ) { this.renderer = rendererFactory.createRenderer(null, null) } @@ -156,6 +158,7 @@ export class SettingsService { let settings$ = this.http.get>(this.baseUrl) settings$.pipe(first()).subscribe((response) => { Object.assign(this.settings, response['settings']) + this.maybeMigrateSettings() }) return settings$ } @@ -447,4 +450,39 @@ export class SettingsService { storeSettings(): Observable { return this.http.post(this.baseUrl, { settings: this.settings }) } + + maybeMigrateSettings() { + if ( + !this.settings.hasOwnProperty('documentListSize') && + localStorage.getItem(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) + ) { + // lets migrate + const successMessage = $localize`Successfully completed one-time migratration of settings to the database!` + const errorMessage = $localize`Unable to migrate settings to the database, please try saving manually.` + + try { + for (const setting in SETTINGS_KEYS) { + const key = SETTINGS_KEYS[setting] + const value = localStorage.getItem(key) + this.set(key, value) + } + } catch (error) { + this.toastService.showError(errorMessage) + console.log(error) + } + + this.storeSettings() + .pipe(first()) + .subscribe({ + next: () => { + this.updateAppearanceSettings() + this.toastService.showInfo(successMessage) + }, + error: (e) => { + this.toastService.showError(errorMessage) + console.log(e) + }, + }) + } + } } From 35a558ec0101b4474bc48dd68fadd08caed31490 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 02:14:20 -0700 Subject: [PATCH 08/24] include language in db settings --- src-ui/src/app/services/settings.service.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index f624f953d..e83ee1415 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -38,6 +38,8 @@ export interface LanguageOption { } export const SETTINGS_KEYS = { + LANGUAGE: 'language', + // maintain old general-settings: for backwards compatibility BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs', BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', @@ -60,6 +62,11 @@ export const SETTINGS_KEYS = { } const SETTINGS: PaperlessSettings[] = [ + { + key: SETTINGS_KEYS.LANGUAGE, + type: 'string', + default: '', + }, { key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: 'boolean', @@ -379,11 +386,13 @@ export class SettingsService { } getLanguage(): string { - return this.cookieService.get(this.getLanguageCookieName()) + return this.get(SETTINGS_KEYS.LANGUAGE) } setLanguage(language: string) { - if (language) { + this.set(SETTINGS_KEYS.LANGUAGE, language) + if (language?.length) { + // for Django this.cookieService.set(this.getLanguageCookieName(), language) } else { this.cookieService.delete(this.getLanguageCookieName()) @@ -466,6 +475,10 @@ export class SettingsService { const value = localStorage.getItem(key) this.set(key, value) } + this.set( + SETTINGS_KEYS.LANGUAGE, + this.cookieService.get(this.getLanguageCookieName()) + ) } catch (error) { this.toastService.showError(errorMessage) console.log(error) From b2307d911eb18bfdc34d469864f2e8f43653d14b Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 07:17:03 -0700 Subject: [PATCH 09/24] update e2e tests for settings initialization --- .../fixtures/frontend_settings/settings.json | 29 +++++++ .../integration/document-detail.spec.ts | 3 + .../integration/documents-list.spec.ts | 3 + src-ui/cypress/integration/manage.spec.ts | 3 + src-ui/cypress/integration/settings.spec.ts | 76 ++++++++++--------- 5 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 src-ui/cypress/fixtures/frontend_settings/settings.json diff --git a/src-ui/cypress/fixtures/frontend_settings/settings.json b/src-ui/cypress/fixtures/frontend_settings/settings.json new file mode 100644 index 000000000..9a92ba410 --- /dev/null +++ b/src-ui/cypress/fixtures/frontend_settings/settings.json @@ -0,0 +1,29 @@ +{ + "bulk_edit": { + "apply_on_close": false, + "confirmation_dialogs": true + }, + "dark_mode": { + "enabled": "false", + "thumb_inverted": "true", + "use_system": true + }, + "date_display": { + "date_format": "mediumDate", + "date_locale": "" + }, + "documentListSize": 50, + "document_details": { + "native_pdf_viewer": false + }, + "language": "", + "notifications": { + "consumer_failed": true, + "consumer_new_documents": true, + "consumer_success": true, + "consumer_suppress_on_dashboard": true + }, + "theme": { + "color": "#cd42cf" + } +} diff --git a/src-ui/cypress/integration/document-detail.spec.ts b/src-ui/cypress/integration/document-detail.spec.ts index 9ff566760..7083ef582 100644 --- a/src-ui/cypress/integration/document-detail.spec.ts +++ b/src-ui/cypress/integration/document-detail.spec.ts @@ -2,6 +2,9 @@ describe('document-detail', () => { beforeEach(() => { this.modifiedDocuments = [] + cy.intercept('http://localhost:8000/api/frontend_settings/', { + fixture: 'frontend_settings/settings.json', + }) cy.fixture('documents/documents.json').then((documentsJson) => { cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { let response = { ...documentsJson } diff --git a/src-ui/cypress/integration/documents-list.spec.ts b/src-ui/cypress/integration/documents-list.spec.ts index 5b923ed2f..45d292fc7 100644 --- a/src-ui/cypress/integration/documents-list.spec.ts +++ b/src-ui/cypress/integration/documents-list.spec.ts @@ -3,6 +3,9 @@ describe('documents-list', () => { this.bulkEdits = {} // mock API methods + cy.intercept('http://localhost:8000/api/frontend_settings/', { + fixture: 'frontend_settings/settings.json', + }) cy.fixture('documents/documents.json').then((documentsJson) => { // bulk edit cy.intercept( diff --git a/src-ui/cypress/integration/manage.spec.ts b/src-ui/cypress/integration/manage.spec.ts index e28556a24..b098ea6a5 100644 --- a/src-ui/cypress/integration/manage.spec.ts +++ b/src-ui/cypress/integration/manage.spec.ts @@ -1,5 +1,8 @@ describe('manage', () => { beforeEach(() => { + cy.intercept('http://localhost:8000/api/frontend_settings/', { + fixture: 'frontend_settings/settings.json', + }) cy.intercept('http://localhost:8000/api/correspondents/*', { fixture: 'correspondents/correspondents.json', }) diff --git a/src-ui/cypress/integration/settings.spec.ts b/src-ui/cypress/integration/settings.spec.ts index 72f9835f2..8350ae035 100644 --- a/src-ui/cypress/integration/settings.spec.ts +++ b/src-ui/cypress/integration/settings.spec.ts @@ -3,45 +3,53 @@ describe('settings', () => { this.modifiedViews = [] // mock API methods - cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { - // saved views PATCH - cy.intercept( - 'PATCH', - 'http://localhost:8000/api/saved_views/*', - (req) => { - this.modifiedViews.push(req.body) // store this for later - req.reply({ result: 'OK' }) - } - ) + cy.intercept('http://localhost:8000/api/frontend_settings/', { + fixture: 'frontend_settings/settings.json', + }).then(() => { + cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { + // saved views PATCH + cy.intercept( + 'PATCH', + 'http://localhost:8000/api/saved_views/*', + (req) => { + this.modifiedViews.push(req.body) // store this for later + req.reply({ result: 'OK' }) + } + ) - cy.intercept('GET', 'http://localhost:8000/api/saved_views/*', (req) => { - let response = { ...savedViewsJson } - if (this.modifiedViews.length) { - response.results = response.results.map((v) => { - if (this.modifiedViews.find((mv) => mv.id == v.id)) - v = this.modifiedViews.find((mv) => mv.id == v.id) - return v - }) - } + cy.intercept( + 'GET', + 'http://localhost:8000/api/saved_views/*', + (req) => { + let response = { ...savedViewsJson } + if (this.modifiedViews.length) { + response.results = response.results.map((v) => { + if (this.modifiedViews.find((mv) => mv.id == v.id)) + v = this.modifiedViews.find((mv) => mv.id == v.id) + return v + }) + } - req.reply(response) - }).as('savedViews') - }) - - cy.fixture('documents/documents.json').then((documentsJson) => { - cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { - let response = { ...documentsJson } - response = response.results.find((d) => d.id == 1) - req.reply(response) + req.reply(response) + } + ).as('savedViews') }) - }) - cy.intercept('http://localhost:8000/api/documents/1/metadata/', { - fixture: 'documents/1/metadata.json', - }) + cy.fixture('documents/documents.json').then((documentsJson) => { + cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { + let response = { ...documentsJson } + response = response.results.find((d) => d.id == 1) + req.reply(response) + }) + }) - cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { - fixture: 'documents/1/suggestions.json', + cy.intercept('http://localhost:8000/api/documents/1/metadata/', { + fixture: 'documents/1/metadata.json', + }) + + cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { + fixture: 'documents/1/suggestions.json', + }) }) cy.viewport(1024, 1024) From de89f75707b1fb06134befbba789933ad58d94ce Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 7 May 2022 08:11:10 -0700 Subject: [PATCH 10/24] Refactor to UiSettings --- .../{frontend_settings => ui_settings}/settings.json | 0 src-ui/cypress/integration/document-detail.spec.ts | 4 ++-- src-ui/cypress/integration/documents-list.spec.ts | 4 ++-- src-ui/cypress/integration/manage.spec.ts | 4 ++-- src-ui/cypress/integration/settings.spec.ts | 4 ++-- src-ui/src/app/services/settings.service.ts | 2 +- .../{1019_frontendsettings.py => 1019_uisettings.py} | 4 ++-- src/documents/models.py | 8 ++++---- src/documents/serialisers.py | 12 ++++++------ src/documents/views.py | 10 +++++----- src/paperless/urls.py | 8 ++++---- 11 files changed, 30 insertions(+), 30 deletions(-) rename src-ui/cypress/fixtures/{frontend_settings => ui_settings}/settings.json (100%) rename src/documents/migrations/{1019_frontendsettings.py => 1019_uisettings.py} (91%) diff --git a/src-ui/cypress/fixtures/frontend_settings/settings.json b/src-ui/cypress/fixtures/ui_settings/settings.json similarity index 100% rename from src-ui/cypress/fixtures/frontend_settings/settings.json rename to src-ui/cypress/fixtures/ui_settings/settings.json diff --git a/src-ui/cypress/integration/document-detail.spec.ts b/src-ui/cypress/integration/document-detail.spec.ts index 7083ef582..8faf2412c 100644 --- a/src-ui/cypress/integration/document-detail.spec.ts +++ b/src-ui/cypress/integration/document-detail.spec.ts @@ -2,8 +2,8 @@ describe('document-detail', () => { beforeEach(() => { this.modifiedDocuments = [] - cy.intercept('http://localhost:8000/api/frontend_settings/', { - fixture: 'frontend_settings/settings.json', + cy.intercept('http://localhost:8000/api/ui_settings/', { + fixture: 'ui_settings/settings.json', }) cy.fixture('documents/documents.json').then((documentsJson) => { cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { diff --git a/src-ui/cypress/integration/documents-list.spec.ts b/src-ui/cypress/integration/documents-list.spec.ts index 45d292fc7..169bd333b 100644 --- a/src-ui/cypress/integration/documents-list.spec.ts +++ b/src-ui/cypress/integration/documents-list.spec.ts @@ -3,8 +3,8 @@ describe('documents-list', () => { this.bulkEdits = {} // mock API methods - cy.intercept('http://localhost:8000/api/frontend_settings/', { - fixture: 'frontend_settings/settings.json', + cy.intercept('http://localhost:8000/api/ui_settings/', { + fixture: 'ui_settings/settings.json', }) cy.fixture('documents/documents.json').then((documentsJson) => { // bulk edit diff --git a/src-ui/cypress/integration/manage.spec.ts b/src-ui/cypress/integration/manage.spec.ts index b098ea6a5..ef7e12723 100644 --- a/src-ui/cypress/integration/manage.spec.ts +++ b/src-ui/cypress/integration/manage.spec.ts @@ -1,7 +1,7 @@ describe('manage', () => { beforeEach(() => { - cy.intercept('http://localhost:8000/api/frontend_settings/', { - fixture: 'frontend_settings/settings.json', + cy.intercept('http://localhost:8000/api/ui_settings/', { + fixture: 'ui_settings/settings.json', }) cy.intercept('http://localhost:8000/api/correspondents/*', { fixture: 'correspondents/correspondents.json', diff --git a/src-ui/cypress/integration/settings.spec.ts b/src-ui/cypress/integration/settings.spec.ts index 8350ae035..16e706b63 100644 --- a/src-ui/cypress/integration/settings.spec.ts +++ b/src-ui/cypress/integration/settings.spec.ts @@ -3,8 +3,8 @@ describe('settings', () => { this.modifiedViews = [] // mock API methods - cy.intercept('http://localhost:8000/api/frontend_settings/', { - fixture: 'frontend_settings/settings.json', + cy.intercept('http://localhost:8000/api/ui_settings/', { + fixture: 'ui_settings/settings.json', }).then(() => { cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { // saved views PATCH diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index e83ee1415..81b15e1b0 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -144,7 +144,7 @@ const SETTINGS: PaperlessSettings[] = [ }) export class SettingsService { private renderer: Renderer2 - protected baseUrl: string = environment.apiBaseUrl + 'frontend_settings/' + protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/' private settings: Object = {} diff --git a/src/documents/migrations/1019_frontendsettings.py b/src/documents/migrations/1019_uisettings.py similarity index 91% rename from src/documents/migrations/1019_frontendsettings.py rename to src/documents/migrations/1019_uisettings.py index 1da17c974..edc944a37 100644 --- a/src/documents/migrations/1019_frontendsettings.py +++ b/src/documents/migrations/1019_uisettings.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="FrontendSettings", + name="UiSettings", fields=[ ( "id", @@ -30,7 +30,7 @@ class Migration(migrations.Migration): "user", models.OneToOneField( on_delete=django.db.models.deletion.CASCADE, - related_name="frontend_settings", + related_name="ui_settings", to=settings.AUTH_USER_MODEL, ), ), diff --git a/src/documents/models.py b/src/documents/models.py index 198bf27b2..208938509 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -470,12 +470,12 @@ class FileInfo: # Extending User Model Using a One-To-One Link -class FrontendSettings(models.Model): +class UiSettings(models.Model): user = models.OneToOneField( User, on_delete=models.CASCADE, - related_name="frontend_settings", + related_name="ui_settings", ) settings = models.JSONField(null=True) @@ -484,6 +484,6 @@ class FrontendSettings(models.Model): @receiver(post_save, sender=User) -def create_user_frontend_settings(sender, instance, created, **kwargs): +def create_user_ui_settings(sender, instance, created, **kwargs): if created: - FrontendSettings.objects.create(user=instance) + UiSettings.objects.create(user=instance) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 9119779e8..a26b67e43 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -11,11 +11,11 @@ from . import bulk_edit from .models import Correspondent from .models import Document from .models import DocumentType -from .models import FrontendSettings from .models import MatchingModel from .models import SavedView from .models import SavedViewFilterRule from .models import Tag +from .models import UiSettings from .parsers import is_mime_type_supported @@ -501,9 +501,9 @@ class BulkDownloadSerializer(DocumentListSerializer): }[compression] -class FrontendSettingsViewSerializer(serializers.ModelSerializer): +class UiSettingsViewSerializer(serializers.ModelSerializer): class Meta: - model = FrontendSettings + model = UiSettings depth = 1 fields = [ "id", @@ -511,12 +511,12 @@ class FrontendSettingsViewSerializer(serializers.ModelSerializer): ] def update(self, instance, validated_data): - super(FrontendSettingsViewSerializer, self).update(instance, validated_data) + super(UiSettingsViewSerializer, self).update(instance, validated_data) return instance def create(self, validated_data): - frontend_settings = FrontendSettings.objects.update_or_create( + ui_settings = UiSettings.objects.update_or_create( user=validated_data.get("user"), defaults={"settings": validated_data.get("settings", None)}, ) - return frontend_settings + return ui_settings diff --git a/src/documents/views.py b/src/documents/views.py index 8b548eed7..a47c7c74f 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -71,11 +71,11 @@ from .serialisers import CorrespondentSerializer from .serialisers import DocumentListSerializer from .serialisers import DocumentSerializer from .serialisers import DocumentTypeSerializer -from .serialisers import FrontendSettingsViewSerializer from .serialisers import PostDocumentSerializer from .serialisers import SavedViewSerializer from .serialisers import TagSerializer from .serialisers import TagSerializerVersion1 +from .serialisers import UiSettingsViewSerializer logger = logging.getLogger("paperless.api") @@ -719,10 +719,10 @@ class RemoteVersionView(GenericAPIView): ) -class FrontendSettingsView(GenericAPIView): +class UiSettingsView(GenericAPIView): permission_classes = (IsAuthenticated,) - serializer_class = FrontendSettingsViewSerializer + serializer_class = UiSettingsViewSerializer def get(self, request, format=None): serializer = self.get_serializer(data=request.data) @@ -730,8 +730,8 @@ class FrontendSettingsView(GenericAPIView): user = User.objects.get(pk=request.user.id) settings = [] - if hasattr(user, "frontend_settings"): - settings = user.frontend_settings.settings + if hasattr(user, "ui_settings"): + settings = user.ui_settings.settings return Response( { "user_id": user.id, diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 0ad9d072f..1e41a5979 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -11,7 +11,6 @@ from documents.views import BulkDownloadView from documents.views import BulkEditView from documents.views import CorrespondentViewSet from documents.views import DocumentTypeViewSet -from documents.views import FrontendSettingsView from documents.views import IndexView from documents.views import LogViewSet from documents.views import PostDocumentView @@ -21,6 +20,7 @@ from documents.views import SearchAutoCompleteView from documents.views import SelectionDataView from documents.views import StatisticsView from documents.views import TagViewSet +from documents.views import UiSettingsView from documents.views import UnifiedSearchViewSet from paperless.consumers import StatusConsumer from paperless.views import FaviconView @@ -80,9 +80,9 @@ urlpatterns = [ name="remoteversion", ), re_path( - r"^frontend_settings/", - FrontendSettingsView.as_view(), - name="frontend_settings", + r"^ui_settings/", + UiSettingsView.as_view(), + name="ui_settings", ), path("token/", views.obtain_auth_token), ] From 96f86adfb87f618f066cc7a025685cc5ace3691e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 9 May 2022 11:01:45 -0700 Subject: [PATCH 11/24] PaperlessUiSettings data object --- src-ui/src/app/app.component.ts | 3 +- .../document-detail.component.ts | 6 +- .../bulk-editor/bulk-editor.component.ts | 6 +- .../document-card-large.component.ts | 8 +- .../document-card-small.component.ts | 6 +- .../manage/settings/settings.component.ts | 2 +- src-ui/src/app/data/paperless-uisettings.ts | 117 +++++++++++++++++ src-ui/src/app/pipes/custom-date.pipe.ts | 3 +- .../services/document-list-view.service.ts | 3 +- src-ui/src/app/services/settings.service.ts | 122 ++---------------- 10 files changed, 143 insertions(+), 133 deletions(-) create mode 100644 src-ui/src/app/data/paperless-uisettings.ts diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index f8c98fbc7..c7954e3a6 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -1,4 +1,5 @@ -import { SettingsService, SETTINGS_KEYS } from './services/settings.service' +import { SettingsService } from './services/settings.service' +import { SETTINGS_KEYS } from './data/paperless-uisettings' import { Component, OnDestroy, OnInit } from '@angular/core' import { Router } from '@angular/router' import { Subscription } from 'rxjs' diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 6f1614e5f..992f5ff06 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -18,10 +18,7 @@ import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document- import { PDFDocumentProxy } from 'ng2-pdf-viewer' import { ToastService } from 'src/app/services/toast.service' import { TextComponent } from '../common/input/text/text.component' -import { - SettingsService, - SETTINGS_KEYS, -} from 'src/app/services/settings.service' +import { SettingsService } from 'src/app/services/settings.service' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { Observable, Subject, BehaviorSubject } from 'rxjs' import { @@ -36,6 +33,7 @@ import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-su import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' import { normalizeDateStr } from 'src/app/utils/date' import { QueryParamsService } from 'src/app/services/query-params.service' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' @Component({ selector: 'app-document-detail', diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index c451f851e..b17af67eb 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -19,12 +19,10 @@ import { } from '../../common/filterable-dropdown/filterable-dropdown.component' import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' import { MatchingModel } from 'src/app/data/matching-model' -import { - SettingsService, - SETTINGS_KEYS, -} from 'src/app/services/settings.service' +import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { saveAs } from 'file-saver' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' @Component({ selector: 'app-bulk-editor', diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index 061a0e681..9b9e20b8c 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -8,12 +8,12 @@ import { } from '@angular/core' import { PaperlessDocument } from 'src/app/data/paperless-document' import { DocumentService } from 'src/app/services/rest/document.service' -import { - SettingsService, - SETTINGS_KEYS, -} from 'src/app/services/settings.service' +import { SettingsService } from 'src/app/services/settings.service' import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' import { OpenDocumentsService } from 'src/app/services/open-documents.service' +import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' @Component({ selector: 'app-document-card-large', diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index c166b42cb..06e2fe967 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -9,12 +9,10 @@ import { import { map } from 'rxjs/operators' import { PaperlessDocument } from 'src/app/data/paperless-document' import { DocumentService } from 'src/app/services/rest/document.service' -import { - SettingsService, - SETTINGS_KEYS, -} from 'src/app/services/settings.service' +import { SettingsService } from 'src/app/services/settings.service' import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' import { OpenDocumentsService } from 'src/app/services/open-documents.service' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' @Component({ selector: 'app-document-card-small', diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index ce999c14c..d9877d281 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -13,11 +13,11 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { LanguageOption, SettingsService, - SETTINGS_KEYS, } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' @Component({ selector: 'app-settings', diff --git a/src-ui/src/app/data/paperless-uisettings.ts b/src-ui/src/app/data/paperless-uisettings.ts new file mode 100644 index 000000000..75aec2a51 --- /dev/null +++ b/src-ui/src/app/data/paperless-uisettings.ts @@ -0,0 +1,117 @@ +export interface PaperlessUiSettings { + user_id: number + + username: string + + display_name: string + + settings: Object +} + +export interface PaperlessUiSetting { + key: string + type: string + default: any +} + +export const SETTINGS_KEYS = { + LANGUAGE: 'language', + // maintain old general-settings: for backwards compatibility + BULK_EDIT_CONFIRMATION_DIALOGS: + 'general-settings:bulk-edit:confirmation-dialogs', + BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', + DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', + DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system', + DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled', + DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted', + THEME_COLOR: 'general-settings:theme:color', + USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer', + DATE_LOCALE: 'general-settings:date-display:date-locale', + DATE_FORMAT: 'general-settings:date-display:date-format', + NOTIFICATIONS_CONSUMER_NEW_DOCUMENT: + 'general-settings:notifications:consumer-new-documents', + NOTIFICATIONS_CONSUMER_SUCCESS: + 'general-settings:notifications:consumer-success', + NOTIFICATIONS_CONSUMER_FAILED: + 'general-settings:notifications:consumer-failed', + NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: + 'general-settings:notifications:consumer-suppress-on-dashboard', +} + +export const SETTINGS: PaperlessUiSetting[] = [ + { + key: SETTINGS_KEYS.LANGUAGE, + type: 'string', + default: '', + }, + { + key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, + type: 'boolean', + default: false, + }, + { + key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, + type: 'number', + default: 50, + }, + { + key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.DARK_MODE_ENABLED, + type: 'boolean', + default: false, + }, + { + key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.THEME_COLOR, + type: 'string', + default: '', + }, + { + key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, + type: 'boolean', + default: false, + }, + { + key: SETTINGS_KEYS.DATE_LOCALE, + type: 'string', + default: '', + }, + { + key: SETTINGS_KEYS.DATE_FORMAT, + type: 'string', + default: 'mediumDate', + }, + { + key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED, + type: 'boolean', + default: true, + }, + { + key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD, + type: 'boolean', + default: true, + }, +] diff --git a/src-ui/src/app/pipes/custom-date.pipe.ts b/src-ui/src/app/pipes/custom-date.pipe.ts index bd4833d04..c9cfc2876 100644 --- a/src-ui/src/app/pipes/custom-date.pipe.ts +++ b/src-ui/src/app/pipes/custom-date.pipe.ts @@ -1,6 +1,7 @@ import { DatePipe } from '@angular/common' import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core' -import { SettingsService, SETTINGS_KEYS } from '../services/settings.service' +import { SETTINGS_KEYS } from '../data/paperless-uisettings' +import { SettingsService } from '../services/settings.service' import { normalizeDateStr } from '../utils/date' const FORMAT_TO_ISO_FORMAT = { diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 99349d37f..471fc7944 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -8,9 +8,10 @@ import { } from '../data/filter-rule' import { PaperlessDocument } from '../data/paperless-document' import { PaperlessSavedView } from '../data/paperless-saved-view' +import { SETTINGS_KEYS } from '../data/paperless-uisettings' import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys' import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service' -import { SettingsService, SETTINGS_KEYS } from './settings.service' +import { SettingsService } from './settings.service' /** * Captures the current state of the list view. diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 81b15e1b0..aeab14df4 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -17,15 +17,13 @@ import { hexToHsl, } from 'src/app/utils/color' import { environment } from 'src/environments/environment' -import { Results } from '../data/results' +import { + PaperlessUiSettings, + SETTINGS, + SETTINGS_KEYS, +} from '../data/paperless-uisettings' import { ToastService } from './toast.service' -export interface PaperlessSettings { - key: string - type: string - default: any -} - export interface LanguageOption { code: string name: string @@ -37,108 +35,6 @@ export interface LanguageOption { dateInputFormat?: string } -export const SETTINGS_KEYS = { - LANGUAGE: 'language', - // maintain old general-settings: for backwards compatibility - BULK_EDIT_CONFIRMATION_DIALOGS: - 'general-settings:bulk-edit:confirmation-dialogs', - BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', - DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', - DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system', - DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled', - DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted', - THEME_COLOR: 'general-settings:theme:color', - USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer', - DATE_LOCALE: 'general-settings:date-display:date-locale', - DATE_FORMAT: 'general-settings:date-display:date-format', - NOTIFICATIONS_CONSUMER_NEW_DOCUMENT: - 'general-settings:notifications:consumer-new-documents', - NOTIFICATIONS_CONSUMER_SUCCESS: - 'general-settings:notifications:consumer-success', - NOTIFICATIONS_CONSUMER_FAILED: - 'general-settings:notifications:consumer-failed', - NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: - 'general-settings:notifications:consumer-suppress-on-dashboard', -} - -const SETTINGS: PaperlessSettings[] = [ - { - key: SETTINGS_KEYS.LANGUAGE, - type: 'string', - default: '', - }, - { - key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, - type: 'boolean', - default: true, - }, - { - key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, - type: 'boolean', - default: false, - }, - { - key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, - type: 'number', - default: 50, - }, - { - key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, - type: 'boolean', - default: true, - }, - { - key: SETTINGS_KEYS.DARK_MODE_ENABLED, - type: 'boolean', - default: false, - }, - { - key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, - type: 'boolean', - default: true, - }, - { - key: SETTINGS_KEYS.THEME_COLOR, - type: 'string', - default: '', - }, - { - key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, - type: 'boolean', - default: false, - }, - { - key: SETTINGS_KEYS.DATE_LOCALE, - type: 'string', - default: '', - }, - { - key: SETTINGS_KEYS.DATE_FORMAT, - type: 'string', - default: 'mediumDate', - }, - { - key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT, - type: 'boolean', - default: true, - }, - { - key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS, - type: 'boolean', - default: true, - }, - { - key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED, - type: 'boolean', - default: true, - }, - { - key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD, - type: 'boolean', - default: true, - }, -] - @Injectable({ providedIn: 'root', }) @@ -161,10 +57,10 @@ export class SettingsService { } // this is called by the app initializer in app.module - public initializeSettings(): Observable> { - let settings$ = this.http.get>(this.baseUrl) - settings$.pipe(first()).subscribe((response) => { - Object.assign(this.settings, response['settings']) + public initializeSettings(): Observable { + let settings$ = this.http.get(this.baseUrl) + settings$.pipe(first()).subscribe((uisettings) => { + Object.assign(this.settings, uisettings.settings) this.maybeMigrateSettings() }) return settings$ From bcb9c6ccb0203d1aa1048acd1760d77d914426f3 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 9 May 2022 11:08:28 -0700 Subject: [PATCH 12/24] Properly get username & display name --- .../app-frame/app-frame.component.html | 10 ++++----- .../app-frame/app-frame.component.ts | 19 +++------------- .../dashboard/dashboard.component.ts | 22 ++++++------------- src-ui/src/app/services/settings.service.ts | 3 +++ src/documents/templates/index.html | 2 -- src/documents/views.py | 5 +++++ 6 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 718e2a242..ce9c8993b 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -21,17 +21,17 @@