mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-28 03:46:06 -05:00 
			
		
		
		
	Merge pull request #1692 from paperless-ngx/feature-frontend-update-checking
Feature: frontend update checking settings
This commit is contained in:
		| @@ -908,18 +908,9 @@ Update Checking | ||||
| ############### | ||||
|  | ||||
| PAPERLESS_ENABLE_UPDATE_CHECK=<bool> | ||||
|     Enable (or disable) the automatic check for available updates. This feature is disabled | ||||
|     by default but if it is not explicitly set Paperless-ngx will show a message about this. | ||||
|  | ||||
|     If enabled, the feature works by pinging the the Github API for the latest release e.g. | ||||
|     https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest | ||||
|     to determine whether a new version is available. | ||||
|     .. note:: | ||||
|  | ||||
|     Actual updating of the app must still be performed manually. | ||||
|  | ||||
|     Note that for users of thirdy-party containers e.g. linuxserver.io this notification | ||||
|     may be 'ahead' of a new release from the third-party maintainers. | ||||
|  | ||||
|     In either case, no tracking data is collected by the app in any way. | ||||
|  | ||||
|     Defaults to none, which disables the feature. | ||||
|             This setting was deprecated in favor of a frontend setting after v1.9.2. A one-time | ||||
|             migration is performed for users who have this setting set. This setting is always | ||||
|             ignored if the corresponding frontend setting has been set. | ||||
|   | ||||
| @@ -46,7 +46,7 @@ describe('settings', () => { | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     cy.viewport(1024, 1024) | ||||
|     cy.viewport(1024, 1280) | ||||
|     cy.visit('/settings') | ||||
|     cy.wait('@savedViews') | ||||
|   }) | ||||
|   | ||||
| @@ -200,14 +200,25 @@ | ||||
|           <li class="nav-item mt-2"> | ||||
|             <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap"> | ||||
|               <div class="me-3">{{ versionString }}</div> | ||||
|               <div *ngIf="appRemoteVersion" class="version-check"> | ||||
|               <div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check"> | ||||
|                 <ng-template #updateAvailablePopContent> | ||||
|                   <span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> | ||||
|                 </ng-template> | ||||
|                 <ng-template #updateCheckingNotEnabledPopContent> | ||||
|                   <span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span> | ||||
|                   <p class="small mb-2"> | ||||
|                     <ng-container i18n>Paperless-ngx can automatically check for updates</ng-container> | ||||
|                   </p> | ||||
|                   <div class="btn-group btn-group-xs flex-fill w-100"> | ||||
|                     <button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button> | ||||
|                     <button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button> | ||||
|                   </div> | ||||
|                   <p class="small mb-0 mt-2"> | ||||
|                     <a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n> | ||||
|                       How does this work? | ||||
|                     </a> | ||||
|                   </p> | ||||
|                 </ng-template> | ||||
|                 <ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet"> | ||||
|                 <ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet"> | ||||
|                   <a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" | ||||
|                   [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||
|                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||
| @@ -217,8 +228,8 @@ | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|                 <ng-template #updateCheckNotSet> | ||||
|                   <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking" | ||||
|                   [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||
|                   <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" | ||||
|                   [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body"> | ||||
|                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||
|                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||
|                     </svg> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, HostListener } from '@angular/core' | ||||
| import { Component, HostListener, OnInit } from '@angular/core' | ||||
| import { FormControl } from '@angular/forms' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { from, Observable } from 'rxjs' | ||||
| @@ -24,13 +24,15 @@ import { | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { TasksService } from 'src/app/services/tasks.service' | ||||
| import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-app-frame', | ||||
|   templateUrl: './app-frame.component.html', | ||||
|   styleUrls: ['./app-frame.component.scss'], | ||||
| }) | ||||
| export class AppFrameComponent implements ComponentCanDeactivate { | ||||
| export class AppFrameComponent implements OnInit, ComponentCanDeactivate { | ||||
|   constructor( | ||||
|     public router: Router, | ||||
|     private activatedRoute: ActivatedRoute, | ||||
| @@ -40,14 +42,15 @@ export class AppFrameComponent implements ComponentCanDeactivate { | ||||
|     private remoteVersionService: RemoteVersionService, | ||||
|     private list: DocumentListViewService, | ||||
|     public settingsService: SettingsService, | ||||
|     public tasksService: TasksService | ||||
|   ) { | ||||
|     this.remoteVersionService | ||||
|       .checkForUpdates() | ||||
|       .subscribe((appRemoteVersion: AppRemoteVersion) => { | ||||
|         this.appRemoteVersion = appRemoteVersion | ||||
|       }) | ||||
|     tasksService.reload() | ||||
|     public tasksService: TasksService, | ||||
|     private readonly toastService: ToastService | ||||
|   ) {} | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) { | ||||
|       this.checkForUpdates() | ||||
|     } | ||||
|     this.tasksService.reload() | ||||
|   } | ||||
|  | ||||
|   versionString = `${environment.appTitle} ${environment.version}` | ||||
| @@ -150,4 +153,30 @@ export class AppFrameComponent implements ComponentCanDeactivate { | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   private checkForUpdates() { | ||||
|     this.remoteVersionService | ||||
|       .checkForUpdates() | ||||
|       .subscribe((appRemoteVersion: AppRemoteVersion) => { | ||||
|         this.appRemoteVersion = appRemoteVersion | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   setUpdateChecking(enable: boolean) { | ||||
|     this.settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, enable) | ||||
|     this.settingsService | ||||
|       .storeSettings() | ||||
|       .pipe(first()) | ||||
|       .subscribe({ | ||||
|         error: (error) => { | ||||
|           this.toastService.showError( | ||||
|             $localize`An error occurred while saving update checking settings.` | ||||
|           ) | ||||
|           console.log(error) | ||||
|         }, | ||||
|       }) | ||||
|     if (enable) { | ||||
|       this.checkForUpdates() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -17,25 +17,6 @@ | ||||
|   } | ||||
| } | ||||
|  | ||||
| .btn-group-xs { | ||||
|   > .btn { | ||||
|     padding: 0.2rem 0.25rem; | ||||
|     font-size: 0.675rem; | ||||
|     line-height: 1.2; | ||||
|     border-radius: 0.15rem; | ||||
|   } | ||||
|  | ||||
|   > .btn:not(:first-child) { | ||||
|     border-top-left-radius: 0; | ||||
|     border-bottom-left-radius: 0; | ||||
|   } | ||||
|  | ||||
|   > .btn:not(:last-child) { | ||||
|     border-top-right-radius: 0; | ||||
|     border-bottom-right-radius: 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .btn-group > label.disabled { | ||||
|   filter: brightness(0.5); | ||||
|  | ||||
|   | ||||
| @@ -116,6 +116,21 @@ | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <h4 class="mt-4" id="update-checking" i18n>Update checking</h4> | ||||
|  | ||||
|         <div class="row mb-3"> | ||||
|           <div class="offset-md-3 col"> | ||||
|             <p i18n> | ||||
|               Update checking works by pinging the the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">Github API</a> for the latest release to determine whether a new version is available.<br/> | ||||
|               Actual updating of the app must still be performed manually. | ||||
|             </p> | ||||
|             <p i18n> | ||||
|               <em>No tracking data is collected by the app in any way.</em> | ||||
|             </p> | ||||
|             <app-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled" i18n-hint hint="Note that for users of thirdy-party containers e.g. linuxserver.io this notification may be 'ahead' of the current third-party release."></app-input-check> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <h4 class="mt-4" i18n>Bulk editing</h4> | ||||
|  | ||||
|         <div class="row mb-3"> | ||||
| @@ -194,5 +209,5 @@ | ||||
|  | ||||
|   <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div> | ||||
|  | ||||
|   <button type="submit" class="btn btn-primary" [disabled]="!(isDirty$ | async)" i18n>Save</button> | ||||
|   <button type="submit" class="btn btn-primary mb-2" [disabled]="!(isDirty$ | async)" i18n>Save</button> | ||||
| </form> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { | ||||
|   LOCALE_ID, | ||||
|   OnInit, | ||||
|   OnDestroy, | ||||
|   Renderer2, | ||||
|   AfterViewInit, | ||||
| } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||
| @@ -16,15 +16,27 @@ import { | ||||
| } from 'src/app/services/settings.service' | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service' | ||||
| import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' | ||||
| import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' | ||||
| import { | ||||
|   Observable, | ||||
|   Subscription, | ||||
|   BehaviorSubject, | ||||
|   first, | ||||
|   tap, | ||||
|   takeUntil, | ||||
|   Subject, | ||||
| } from 'rxjs' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
| import { ActivatedRoute } from '@angular/router' | ||||
| import { ViewportScroller } from '@angular/common' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-settings', | ||||
|   templateUrl: './settings.component.html', | ||||
|   styleUrls: ['./settings.component.scss'], | ||||
| }) | ||||
| export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
| export class SettingsComponent | ||||
|   implements OnInit, AfterViewInit, OnDestroy, DirtyComponent | ||||
| { | ||||
|   savedViewGroup = new FormGroup({}) | ||||
|  | ||||
|   settingsForm = new FormGroup({ | ||||
| @@ -45,6 +57,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|     notificationsConsumerFailed: new FormControl(null), | ||||
|     notificationsConsumerSuppressOnDashboard: new FormControl(null), | ||||
|     commentsEnabled: new FormControl(null), | ||||
|     updateCheckingEnabled: new FormControl(null), | ||||
|   }) | ||||
|  | ||||
|   savedViews: PaperlessSavedView[] | ||||
| @@ -52,7 +65,9 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|   store: BehaviorSubject<any> | ||||
|   storeSub: Subscription | ||||
|   isDirty$: Observable<boolean> | ||||
|   isDirty: Boolean = false | ||||
|   isDirty: boolean = false | ||||
|   unsubscribeNotifier: Subject<any> = new Subject() | ||||
|   savePending: boolean = false | ||||
|  | ||||
|   get computedDateLocale(): string { | ||||
|     return ( | ||||
| @@ -62,24 +77,38 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   get displayLanguageIsDirty(): boolean { | ||||
|     return ( | ||||
|       this.settingsForm.get('displayLanguage').value != | ||||
|       this.store?.getValue()['displayLanguage'] | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   constructor( | ||||
|     public savedViewService: SavedViewService, | ||||
|     private documentListViewService: DocumentListViewService, | ||||
|     private toastService: ToastService, | ||||
|     private settings: SettingsService, | ||||
|     @Inject(LOCALE_ID) public currentLocale: string | ||||
|   ) {} | ||||
|     @Inject(LOCALE_ID) public currentLocale: string, | ||||
|     private viewportScroller: ViewportScroller, | ||||
|     private activatedRoute: ActivatedRoute | ||||
|   ) { | ||||
|     this.settings.settingsSaved.subscribe(() => { | ||||
|       if (!this.savePending) this.initialize() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   ngAfterViewInit(): void { | ||||
|     if (this.activatedRoute.snapshot.fragment) { | ||||
|       this.viewportScroller.scrollToAnchor( | ||||
|         this.activatedRoute.snapshot.fragment | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.savedViewService.listAll().subscribe((r) => { | ||||
|       this.savedViews = r.results | ||||
|       this.initialize() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   initialize() { | ||||
|     this.unsubscribeNotifier.next(true) | ||||
|  | ||||
|     let storeData = { | ||||
|       bulkEditConfirmationDialogs: this.settings.get( | ||||
|         SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS | ||||
| @@ -90,9 +119,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|       documentListItemPerPage: this.settings.get( | ||||
|         SETTINGS_KEYS.DOCUMENT_LIST_SIZE | ||||
|       ), | ||||
|         darkModeUseSystem: this.settings.get( | ||||
|           SETTINGS_KEYS.DARK_MODE_USE_SYSTEM | ||||
|         ), | ||||
|       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 | ||||
| @@ -118,6 +145,9 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD | ||||
|       ), | ||||
|       commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED), | ||||
|       updateCheckingEnabled: this.settings.get( | ||||
|         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED | ||||
|       ), | ||||
|     } | ||||
|  | ||||
|     for (let view of this.savedViews) { | ||||
| @@ -148,19 +178,22 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|     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$ | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((dirty) => { | ||||
|         this.isDirty = dirty | ||||
|       }) | ||||
|  | ||||
|     // "Live" visual changes prior to save | ||||
|       this.settingsForm.valueChanges.subscribe(() => { | ||||
|     this.settingsForm.valueChanges | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         this.settings.updateAppearanceSettings( | ||||
|           this.settingsForm.get('darkModeUseSystem').value, | ||||
|           this.settingsForm.get('darkModeEnabled').value, | ||||
|           this.settingsForm.get('themeColor').value | ||||
|         ) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
| @@ -179,7 +212,14 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|   } | ||||
|  | ||||
|   private saveLocalSettings() { | ||||
|     const reloadRequired = this.displayLanguageIsDirty // just this one, for now | ||||
|     this.savePending = true | ||||
|     const reloadRequired = | ||||
|       this.settingsForm.value.displayLanguage != | ||||
|         this.store?.getValue()['displayLanguage'] || // displayLanguage is dirty | ||||
|       (this.settingsForm.value.updateCheckingEnabled != | ||||
|         this.store?.getValue()['updateCheckingEnabled'] && | ||||
|         this.settingsForm.value.updateCheckingEnabled) // update checking was turned on | ||||
|  | ||||
|     this.settings.set( | ||||
|       SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, | ||||
|       this.settingsForm.value.bulkEditApplyOnClose | ||||
| @@ -240,10 +280,15 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | ||||
|       SETTINGS_KEYS.COMMENTS_ENABLED, | ||||
|       this.settingsForm.value.commentsEnabled | ||||
|     ) | ||||
|     this.settings.set( | ||||
|       SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, | ||||
|       this.settingsForm.value.updateCheckingEnabled | ||||
|     ) | ||||
|     this.settings.setLanguage(this.settingsForm.value.displayLanguage) | ||||
|     this.settings | ||||
|       .storeSettings() | ||||
|       .pipe(first()) | ||||
|       .pipe(tap(() => (this.savePending = false))) | ||||
|       .subscribe({ | ||||
|         next: () => { | ||||
|           this.store.next(this.settingsForm.value) | ||||
|   | ||||
| @@ -37,6 +37,9 @@ export const SETTINGS_KEYS = { | ||||
|   NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: | ||||
|     'general-settings:notifications:consumer-suppress-on-dashboard', | ||||
|   COMMENTS_ENABLED: 'general-settings:comments-enabled', | ||||
|   UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled', | ||||
|   UPDATE_CHECKING_BACKEND_SETTING: | ||||
|     'general-settings:update-checking:backend-setting', | ||||
| } | ||||
|  | ||||
| export const SETTINGS: PaperlessUiSetting[] = [ | ||||
| @@ -120,4 +123,14 @@ export const SETTINGS: PaperlessUiSetting[] = [ | ||||
|     type: 'boolean', | ||||
|     default: true, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, | ||||
|     type: 'boolean', | ||||
|     default: false, | ||||
|   }, | ||||
|   { | ||||
|     key: SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING, | ||||
|     type: 'string', | ||||
|     default: '', | ||||
|   }, | ||||
| ] | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import { environment } from 'src/environments/environment' | ||||
| export interface AppRemoteVersion { | ||||
|   version: string | ||||
|   update_available: boolean | ||||
|   feature_is_set: boolean | ||||
| } | ||||
|  | ||||
| @Injectable({ | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { DOCUMENT } from '@angular/common' | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { | ||||
|   EventEmitter, | ||||
|   Inject, | ||||
|   Injectable, | ||||
|   LOCALE_ID, | ||||
| @@ -46,6 +47,8 @@ export class SettingsService { | ||||
|  | ||||
|   public displayName: string | ||||
|  | ||||
|   public settingsSaved: EventEmitter<any> = new EventEmitter() | ||||
|  | ||||
|   constructor( | ||||
|     rendererFactory: RendererFactory2, | ||||
|     @Inject(DOCUMENT) private document, | ||||
| @@ -313,13 +316,7 @@ export class SettingsService { | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   get(key: string): any { | ||||
|     let setting = SETTINGS.find((s) => s.key == key) | ||||
|  | ||||
|     if (!setting) { | ||||
|       return null | ||||
|     } | ||||
|  | ||||
|   private getSettingRawValue(key: string): any { | ||||
|     let value = null | ||||
|     // parse key:key:key into nested object | ||||
|     const keys = key.replace('general-settings:', '').split(':') | ||||
| @@ -330,6 +327,17 @@ export class SettingsService { | ||||
|       if (index == keys.length - 1) value = settingObj[keyPart] | ||||
|       else settingObj = settingObj[keyPart] | ||||
|     }) | ||||
|     return value | ||||
|   } | ||||
|  | ||||
|   get(key: string): any { | ||||
|     let setting = SETTINGS.find((s) => s.key == key) | ||||
|  | ||||
|     if (!setting) { | ||||
|       return null | ||||
|     } | ||||
|  | ||||
|     let value = this.getSettingRawValue(key) | ||||
|  | ||||
|     if (value != null) { | ||||
|       switch (setting.type) { | ||||
| @@ -359,8 +367,19 @@ export class SettingsService { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   private settingIsSet(key: string): boolean { | ||||
|     let value = this.getSettingRawValue(key) | ||||
|     return value != null | ||||
|   } | ||||
|  | ||||
|   storeSettings(): Observable<any> { | ||||
|     return this.http.post(this.baseUrl, { settings: this.settings }) | ||||
|     return this.http.post(this.baseUrl, { settings: this.settings }).pipe( | ||||
|       tap((results) => { | ||||
|         if (results.success) { | ||||
|           this.settingsSaved.emit() | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   maybeMigrateSettings() { | ||||
| @@ -400,5 +419,31 @@ export class SettingsService { | ||||
|           }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       !this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) && | ||||
|       this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING) != 'default' | ||||
|     ) { | ||||
|       this.set( | ||||
|         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, | ||||
|         this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING).toString() === | ||||
|           'true' | ||||
|       ) | ||||
|  | ||||
|       this.storeSettings() | ||||
|         .pipe(first()) | ||||
|         .subscribe({ | ||||
|           error: (e) => { | ||||
|             this.toastService.showError( | ||||
|               'Error migrating update checking setting' | ||||
|             ) | ||||
|             console.log(e) | ||||
|           }, | ||||
|         }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get updateCheckingIsSet(): boolean { | ||||
|     return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -526,6 +526,25 @@ a.badge { | ||||
|     border-color: var(--bs-primary); | ||||
| } | ||||
|  | ||||
| .btn-group-xs { | ||||
|   > .btn { | ||||
|     padding: 0.2rem 0.25rem; | ||||
|     font-size: 0.675rem; | ||||
|     line-height: 1.2; | ||||
|     border-radius: 0.15rem; | ||||
|   } | ||||
|  | ||||
|   > .btn:not(:first-child) { | ||||
|     border-top-left-radius: 0; | ||||
|     border-bottom-left-radius: 0; | ||||
|   } | ||||
|  | ||||
|   > .btn:not(:last-child) { | ||||
|     border-top-right-radius: 0; | ||||
|     border-bottom-right-radius: 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| code { | ||||
|   color: var(--pngx-body-color-accent) | ||||
| } | ||||
|   | ||||
| @@ -608,6 +608,15 @@ class UiSettingsViewSerializer(serializers.ModelSerializer): | ||||
|             "settings", | ||||
|         ] | ||||
|  | ||||
|     def validate_settings(self, settings): | ||||
|         # we never save update checking backend setting | ||||
|         if "update_checking" in settings: | ||||
|             try: | ||||
|                 settings["update_checking"].pop("backend_setting") | ||||
|             except KeyError: | ||||
|                 pass | ||||
|         return settings | ||||
|  | ||||
|     def create(self, validated_data): | ||||
|         ui_settings = UiSettings.objects.update_or_create( | ||||
|             user=validated_data.get("user"), | ||||
|   | ||||
| @@ -1581,7 +1581,11 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase): | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertDictEqual( | ||||
|             response.data["settings"], | ||||
|             {}, | ||||
|             { | ||||
|                 "update_checking": { | ||||
|                     "backend_setting": "default", | ||||
|                 }, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_api_set_ui_settings(self): | ||||
| @@ -2542,38 +2546,6 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase): | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|  | ||||
|     def test_remote_version_default(self): | ||||
|         response = self.client.get(self.ENDPOINT) | ||||
|  | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertDictEqual( | ||||
|             response.data, | ||||
|             { | ||||
|                 "version": "0.0.0", | ||||
|                 "update_available": False, | ||||
|                 "feature_is_set": False, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         ENABLE_UPDATE_CHECK=False, | ||||
|     ) | ||||
|     def test_remote_version_disabled(self): | ||||
|         response = self.client.get(self.ENDPOINT) | ||||
|  | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertDictEqual( | ||||
|             response.data, | ||||
|             { | ||||
|                 "version": "0.0.0", | ||||
|                 "update_available": False, | ||||
|                 "feature_is_set": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         ENABLE_UPDATE_CHECK=True, | ||||
|     ) | ||||
|     @mock.patch("urllib.request.urlopen") | ||||
|     def test_remote_version_enabled_no_update_prefix(self, urlopen_mock): | ||||
|  | ||||
| @@ -2591,13 +2563,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase): | ||||
|             { | ||||
|                 "version": "1.6.0", | ||||
|                 "update_available": False, | ||||
|                 "feature_is_set": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         ENABLE_UPDATE_CHECK=True, | ||||
|     ) | ||||
|     @mock.patch("urllib.request.urlopen") | ||||
|     def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock): | ||||
|  | ||||
| @@ -2617,13 +2585,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase): | ||||
|             { | ||||
|                 "version": version.__full_version_str__, | ||||
|                 "update_available": False, | ||||
|                 "feature_is_set": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         ENABLE_UPDATE_CHECK=True, | ||||
|     ) | ||||
|     @mock.patch("urllib.request.urlopen") | ||||
|     def test_remote_version_enabled_update(self, urlopen_mock): | ||||
|  | ||||
| @@ -2650,13 +2614,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase): | ||||
|             { | ||||
|                 "version": new_version_str, | ||||
|                 "update_available": True, | ||||
|                 "feature_is_set": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         ENABLE_UPDATE_CHECK=True, | ||||
|     ) | ||||
|     @mock.patch("urllib.request.urlopen") | ||||
|     def test_remote_version_bad_json(self, urlopen_mock): | ||||
|  | ||||
| @@ -2674,13 +2634,9 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase): | ||||
|             { | ||||
|                 "version": "0.0.0", | ||||
|                 "update_available": False, | ||||
|                 "feature_is_set": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         ENABLE_UPDATE_CHECK=True, | ||||
|     ) | ||||
|     @mock.patch("urllib.request.urlopen") | ||||
|     def test_remote_version_exception(self, urlopen_mock): | ||||
|  | ||||
| @@ -2698,7 +2654,6 @@ class TestApiRemoteVersion(DirectoriesMixin, APITestCase): | ||||
|             { | ||||
|                 "version": "0.0.0", | ||||
|                 "update_available": False, | ||||
|                 "feature_is_set": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -783,9 +783,6 @@ class RemoteVersionView(GenericAPIView): | ||||
|         remote_version = "0.0.0" | ||||
|         is_greater_than_current = False | ||||
|         current_version = packaging_version.parse(version.__full_version_str__) | ||||
|         # TODO: this can likely be removed when frontend settings are saved to DB | ||||
|         feature_is_set = settings.ENABLE_UPDATE_CHECK != "default" | ||||
|         if feature_is_set and settings.ENABLE_UPDATE_CHECK: | ||||
|         try: | ||||
|             req = urllib.request.Request( | ||||
|                 "https://api.github.com/repos/paperless-ngx/" | ||||
| @@ -818,7 +815,6 @@ class RemoteVersionView(GenericAPIView): | ||||
|             { | ||||
|                 "version": remote_version, | ||||
|                 "update_available": is_greater_than_current, | ||||
|                 "feature_is_set": feature_is_set, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @@ -851,15 +847,23 @@ class UiSettingsView(GenericAPIView): | ||||
|         displayname = user.username | ||||
|         if user.first_name or user.last_name: | ||||
|             displayname = " ".join([user.first_name, user.last_name]) | ||||
|         settings = {} | ||||
|         ui_settings = {} | ||||
|         if hasattr(user, "ui_settings"): | ||||
|             settings = user.ui_settings.settings | ||||
|             ui_settings = user.ui_settings.settings | ||||
|         if "update_checking" in ui_settings: | ||||
|             ui_settings["update_checking"][ | ||||
|                 "backend_setting" | ||||
|             ] = settings.ENABLE_UPDATE_CHECK | ||||
|         else: | ||||
|             ui_settings["update_checking"] = { | ||||
|                 "backend_setting": settings.ENABLE_UPDATE_CHECK, | ||||
|             } | ||||
|         return Response( | ||||
|             { | ||||
|                 "user_id": user.id, | ||||
|                 "username": user.username, | ||||
|                 "display_name": displayname, | ||||
|                 "settings": settings, | ||||
|                 "settings": ui_settings, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon