From b74ecf8ede7330b0d3dbb6bdaaf0eb446379f0cb Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 29 Jan 2019 10:51:01 -0500 Subject: [PATCH] LP1702929 Save grid settings as AOUS WIP Signed-off-by: Bill Erickson --- Open-ILS/src/eg2/src/app/common.module.ts | 3 + .../src/eg2/src/app/core/server-store.service.ts | 34 ++++- .../share/grid/grid-aous-org-select.component.html | 39 ++++++ .../share/grid/grid-aous-org-select.component.ts | 38 ++++++ .../src/app/share/grid/grid-toolbar.component.html | 10 ++ .../src/app/share/grid/grid-toolbar.component.ts | 17 ++- .../src/eg2/src/app/share/grid/grid.component.ts | 6 +- Open-ILS/src/eg2/src/app/share/grid/grid.module.ts | 4 +- Open-ILS/src/eg2/src/app/share/grid/grid.ts | 33 ++++- Open-ILS/src/eg2/src/app/staff/common.module.ts | 3 - Open-ILS/src/sql/Pg/005.schema.actors.sql | 15 ++- .../upgrade/XXXX.schema.setting-summary-perms.sql | 147 +++++++++++++++++++++ 12 files changed, 330 insertions(+), 19 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.html create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.ts create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.setting-summary-perms.sql diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts index c83ad392bc..0f37a0e1f8 100644 --- a/Open-ILS/src/eg2/src/app/common.module.ts +++ b/Open-ILS/src/eg2/src/app/common.module.ts @@ -19,6 +19,7 @@ import {PrintService} from '@eg/share/print/print.service'; // Globally available components import {PrintComponent} from '@eg/share/print/print.component'; import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {OrgSelectComponent} from '@eg/share/org-select/org-select.component'; import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; import {PromptDialogComponent} from '@eg/share/dialog/prompt.component'; import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component'; @@ -27,6 +28,7 @@ import {ProgressDialogComponent} from '@eg/share/dialog/progress.component'; @NgModule({ declarations: [ PrintComponent, + OrgSelectComponent, DialogComponent, ConfirmDialogComponent, PromptDialogComponent, @@ -45,6 +47,7 @@ import {ProgressDialogComponent} from '@eg/share/dialog/progress.component'; NgbModule, FormsModule, PrintComponent, + OrgSelectComponent, DialogComponent, ConfirmDialogComponent, PromptDialogComponent, diff --git a/Open-ILS/src/eg2/src/app/core/server-store.service.ts b/Open-ILS/src/eg2/src/app/core/server-store.service.ts index 43415c1951..1485b811ed 100644 --- a/Open-ILS/src/eg2/src/app/core/server-store.service.ts +++ b/Open-ILS/src/eg2/src/app/core/server-store.service.ts @@ -12,6 +12,8 @@ interface ServerSettingSummary { has_org_setting: boolean; has_user_setting: boolean; has_workstation_setting: boolean; + aous_view_perm: string; + aous_update_perm: string; } @Injectable({providedIn: 'root'}) @@ -42,7 +44,18 @@ export class ServerStoreService { .toPromise().then(appliedCount => { if (Number(appliedCount) > 0) { // value applied - return this.cache[key] = value; + + if (this.cache[key]) { + // Update the cached value + return this.cache[key].value = value; + } + + // Typically, a copy of the setting summary would have + // been fetched/cached before a new value is applied, + // but it's not guaranteed. If no local summary exists + // at apply-time, fetch the summary now so we'll have + // an accurate representation of the summary. + return this.getItem(key).then(() => value); } return Promise.reject( @@ -66,7 +79,7 @@ export class ServerStoreService { const values: any = {}; keys.forEach(key => { if (this.cache[key]) { - values[key] = this.cache[key]; + values[key] = this.cache[key].value; } }); @@ -98,8 +111,12 @@ export class ServerStoreService { serverKeys, this.auth.token() ).subscribe( summary => { - this.cache[summary.name] = - values[summary.name] = summary.value; + this.cache[summary.name] = summary; + values[summary.name] = summary.value; + // boolean-ize the bools + summary.has_org_setting = summary.has_org_setting === 't'; + summary.has_user_setting = summary.has_user_setting === 't'; + summary.has_workstation_set = summary.has_workstation_set === 't'; }, err => reject, () => resolve(values) @@ -110,5 +127,14 @@ export class ServerStoreService { removeItem(key: string): Promise { return this.setItem(key, null); } + + getSettingSummary(key: string): Promise { + if (this.cache[key]) { + return Promise.resolve(this.cache[key]); + } + + // Setting summary not yet cached, fetch it + return this.getItem(key).then(() => this.cache[key]); + } } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.html new file mode 100644 index 0000000000..0296ab1656 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.html @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.ts new file mode 100644 index 0000000000..27ba3687c6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.ts @@ -0,0 +1,38 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {GridContext} from './grid'; + +@Component({ + selector: 'eg-grid-aous-org-select', + templateUrl: './grid-aous-org-select.component.html' +}) + +/** + */ +export class GridAousOrgSelectComponent extends DialogComponent implements OnInit { + @Input() context: GridContext; + + settingsAsString(): string { + if (this.context) { + return JSON.stringify( + this.context.columnSet.compileSaveObject(), null, 2); + } + } + + // TODO + /* + hideTheseOrgs(): number[] { + const orgs = []; + if (this.context) { + + this.context.storeSettingsAsAousOrgs.forEach(ouId => { + if ( + + } else { + return []; + } + } + */ +} + + diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html index 5eaa81ff62..d97b1e8391 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html @@ -1,6 +1,9 @@
+ + +
@@ -118,6 +121,13 @@ save Save Grid Settings + + save + Save Grid Settings As Library Setting + + restore diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts index 399a4c7211..60ac42511b 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts @@ -1,6 +1,7 @@ import {Component, Input, OnInit, Host} from '@angular/core'; import {DomSanitizer, SafeUrl} from '@angular/platform-browser'; import {Pager} from '@eg/share/util/pager'; +import {ServerStoreService} from '@eg/core/server-store.service'; import {GridColumn, GridColumnSet, GridToolbarButton, GridToolbarAction, GridContext, GridDataSource} from '@eg/share/grid/grid'; import {GridColumnWidthComponent} from './grid-column-width.component'; @@ -21,15 +22,14 @@ export class GridToolbarComponent implements OnInit { csvExportUrl: SafeUrl; csvExportFileName: string; - constructor(private sanitizer: DomSanitizer) {} + constructor( + private sanitizer: DomSanitizer, + private store: ServerStoreService + ) {} ngOnInit() {} saveGridConfig() { - // TODO: when server-side settings are supported, this operation - // may offer to save to user/workstation OR org unit settings - // depending on perms. - this.gridContext.saveGridConfig().then( // hide the with config after saving ok => this.colWidthConfig.isVisible = false, @@ -37,6 +37,13 @@ export class GridToolbarComponent implements OnInit { ); } + // Display a dialog where the user can select the org unit at + // which they want to apply the grid configuration. + // This action is only available if the permission to update the org + // unit setting in question has already been confirmed. + saveConfigAsAous() { + } + performAction(action: GridToolbarAction) { action.action(this.gridContext.getSelectedRows()); } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts index 1fa4c2cfee..52b82ab5f4 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts @@ -3,6 +3,7 @@ import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter, import {Subscription} from 'rxjs/Subscription'; import {IdlService} from '@eg/core/idl.service'; import {OrgService} from '@eg/core/org.service'; +import {PermService} from '@eg/core/perm.service'; import {ServerStoreService} from '@eg/core/server-store.service'; import {FormatService} from '@eg/core/format.service'; import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid'; @@ -89,10 +90,11 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy { private idl: IdlService, private org: OrgService, private store: ServerStoreService, + private perms: PermService, private format: FormatService ) { - this.context = - new GridContext(this.idl, this.org, this.store, this.format); + this.context = new GridContext( + this.idl, this.org, this.store, this.format, this.perms); this.onRowActivate = new EventEmitter(); this.onRowClick = new EventEmitter(); } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts index 0773a7e56f..24b718426c 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts @@ -12,6 +12,7 @@ import {GridToolbarActionComponent} from './grid-toolbar-action.component'; import {GridColumnConfigComponent} from './grid-column-config.component'; import {GridColumnWidthComponent} from './grid-column-width.component'; import {GridPrintComponent} from './grid-print.component'; +import {GridAousOrgSelectComponent} from './grid-aous-org-select.component'; @NgModule({ @@ -28,7 +29,8 @@ import {GridPrintComponent} from './grid-print.component'; GridToolbarActionComponent, GridColumnConfigComponent, GridColumnWidthComponent, - GridPrintComponent + GridPrintComponent, + GridAousOrgSelectComponent ], imports: [ EgCommonModule diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts index 8a777e4b56..20a62b92d0 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -6,6 +6,7 @@ import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; import {IdlService, IdlObject} from '@eg/core/idl.service'; import {OrgService} from '@eg/core/org.service'; +import {PermService} from '@eg/core/perm.service'; import {ServerStoreService} from '@eg/core/server-store.service'; import {FormatService} from '@eg/core/format.service'; import {Pager} from '@eg/share/util/pager'; @@ -438,21 +439,29 @@ export class GridContext { defaultHiddenFields: string[]; overflowCells: boolean; + // List of org unit IDs where the logged in user is allowed to + // save a value to the org unit setting whose name matches this + // grid's persist key. + storeSettingsAsAousOrgs: number[]; + // Services injected by our grid component idl: IdlService; org: OrgService; store: ServerStoreService; format: FormatService; + perms: PermService; constructor( idl: IdlService, org: OrgService, store: ServerStoreService, - format: FormatService) { + format: FormatService, + perms: PermService) { this.idl = idl; this.org = org; this.store = store; + this.perms = perms; this.format = format; this.pager = new Pager(); this.pager.limit = 10; @@ -460,6 +469,7 @@ export class GridContext { this.toolbarButtons = []; this.toolbarCheckboxes = []; this.toolbarActions = []; + this.storeSettingsAsAousOrgs = []; } init() { @@ -496,9 +506,28 @@ export class GridContext { // This is called regardless of the presence of saved // settings so defaults can be applied. this.columnSet.applyColumnSettings(columns); + this.checkStoreSettingAsAous(); }); } + + // If the grid column configuration may be saved as an org unit + // setting value and the logged in user has permission to apply said + // value, display the save-as option in the grid settings menu. + checkStoreSettingAsAous() { + + this.store.getSettingSummary('eg.grid.' + this.persistKey).then( + summary => { + if (summary && summary.has_org_setting) { + const perm = summary.aous_update_perm + || 'UPDATE_ORG_UNIT_SETTING_ALL'; + this.perms.hasWorkPermAt([perm], true).then( + perms => this.storeSettingsAsAousOrgs = perms[perm]); + } + } + ); + } + reload() { // Give the UI time to settle before reloading grid data. // This can help when data retrieval depends on a value @@ -877,8 +906,6 @@ export class GridContext { return this.store.setItem('eg.grid.' + this.persistKey, conf); } - // TODO: saveGridConfigAsOrgSetting(...) - getGridConfig(persistKey: string): Promise { if (!persistKey) { return Promise.resolve(null); } return this.store.getItem('eg.grid.' + persistKey); diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index e83143c9a3..3fafb71682 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -5,7 +5,6 @@ import {GridModule} from '@eg/share/grid/grid.module'; import {StaffBannerComponent} from './share/staff-banner.component'; import {ComboboxComponent} from '@eg/share/combobox/combobox.component'; import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component'; -import {OrgSelectComponent} from '@eg/share/org-select/org-select.component'; import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive'; import {AccessKeyService} from '@eg/share/accesskey/accesskey.service'; import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component'; @@ -30,7 +29,6 @@ import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.componen StaffBannerComponent, ComboboxComponent, ComboboxEntryComponent, - OrgSelectComponent, AccessKeyDirective, AccessKeyInfoComponent, ToastComponent, @@ -53,7 +51,6 @@ import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.componen StaffBannerComponent, ComboboxComponent, ComboboxEntryComponent, - OrgSelectComponent, AccessKeyDirective, AccessKeyInfoComponent, ToastComponent, diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql index c3879670b4..e18abd4673 100644 --- a/Open-ILS/src/sql/Pg/005.schema.actors.sql +++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql @@ -1119,7 +1119,9 @@ CREATE TYPE actor.cascade_setting_summary AS ( value JSON, has_org_setting BOOLEAN, has_user_setting BOOLEAN, - has_workstation_setting BOOLEAN + has_workstation_setting BOOLEAN, + aous_view_perm TEXT, + aous_update_perm TEXT ); CREATE OR REPLACE FUNCTION actor.get_cascade_setting( @@ -1141,6 +1143,17 @@ BEGIN FROM config.org_unit_setting_type WHERE name = setting_name; IF FOUND THEN summary.has_org_setting := TRUE; + + -- Return view/update perm codes to the caller + IF org_setting_type.view_perm IS NOT NULL THEN + SELECT code INTO summary.aous_view_perm + FROM permission.perm_list WHERE id = org_setting_type.view_perm; + END IF; + + IF org_setting_type.update_perm IS NOT NULL THEN + SELECT code INTO summary.aous_update_perm + FROM permission.perm_list WHERE id = org_setting_type.update_perm; + END IF; ELSE summary.has_org_setting := FALSE; END IF; diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.setting-summary-perms.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.setting-summary-perms.sql new file mode 100644 index 0000000000..cff71d9bc0 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.setting-summary-perms.sql @@ -0,0 +1,147 @@ +BEGIN; + +-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version); + +ALTER TYPE actor.cascade_setting_summary + ADD ATTRIBUTE aous_view_perm TEXT, + ADD ATTRIBUTE aous_update_perm TEXT; + +CREATE OR REPLACE FUNCTION actor.get_cascade_setting( + setting_name TEXT, org_id INT, user_id INT, workstation_id INT) + RETURNS actor.cascade_setting_summary AS +$FUNC$ +DECLARE + setting_value JSON; + summary actor.cascade_setting_summary; + org_setting_type config.org_unit_setting_type%ROWTYPE; +BEGIN + + summary.name := setting_name; + + -- Collect the org setting type status first in case we exit early. + -- The existance of an org setting type is not considered + -- privileged information. + SELECT INTO org_setting_type * + FROM config.org_unit_setting_type WHERE name = setting_name; + IF FOUND THEN + summary.has_org_setting := TRUE; + + -- Return view/update perm codes to the caller + IF org_setting_type.view_perm IS NOT NULL THEN + SELECT code INTO summary.aous_view_perm + FROM permission.perm_list WHERE id = org_setting_type.view_perm; + END IF; + + IF org_setting_type.update_perm IS NOT NULL THEN + SELECT code INTO summary.aous_update_perm + FROM permission.perm_list WHERE id = org_setting_type.update_perm; + END IF; + ELSE + summary.has_org_setting := FALSE; + END IF; + + -- User and workstation settings have the same priority. + -- Start with user settings since that's the simplest code path. + -- The workstation_id is ignored if no user_id is provided. + IF user_id IS NOT NULL THEN + + SELECT INTO summary.value value FROM actor.usr_setting + WHERE usr = user_id AND name = setting_name; + + IF FOUND THEN + -- if we have a value, we have a setting type + summary.has_user_setting := TRUE; + + IF workstation_id IS NOT NULL THEN + -- Only inform the caller about the workstation + -- setting type disposition when a workstation id is + -- provided. Otherwise, it's NULL to indicate UNKNOWN. + summary.has_workstation_setting := FALSE; + END IF; + + RETURN summary; + END IF; + + -- no user setting value, but a setting type may exist + SELECT INTO summary.has_user_setting EXISTS ( + SELECT TRUE FROM config.usr_setting_type + WHERE name = setting_name + ); + + IF workstation_id IS NOT NULL THEN + + IF NOT summary.has_user_setting THEN + -- A workstation setting type may only exist when a user + -- setting type does not. + + SELECT INTO summary.value value + FROM actor.workstation_setting + WHERE workstation = workstation_id AND name = setting_name; + + IF FOUND THEN + -- if we have a value, we have a setting type + summary.has_workstation_setting := TRUE; + RETURN summary; + END IF; + + -- no value, but a setting type may exist + SELECT INTO summary.has_workstation_setting EXISTS ( + SELECT TRUE FROM config.workstation_setting_type + WHERE name = setting_name + ); + END IF; + + -- Finally make use of the workstation to determine the org + -- unit if none is provided. + IF org_id IS NULL AND summary.has_org_setting THEN + SELECT INTO org_id owning_lib + FROM actor.workstation WHERE id = workstation_id; + END IF; + END IF; + END IF; + + -- Some org unit settings are protected by a view permission. + -- First see if we have any data that needs protecting, then + -- check the permission if needed. + + IF NOT summary.has_org_setting THEN + RETURN summary; + END IF; + + -- avoid putting the value into the summary until we confirm + -- the value should be visible to the caller. + SELECT INTO setting_value value + FROM actor.org_unit_ancestor_setting(setting_name, org_id); + + IF NOT FOUND THEN + -- No value found -- perm check is irrelevant. + RETURN summary; + END IF; + + IF org_setting_type.view_perm IS NOT NULL THEN + + IF user_id IS NULL THEN + RAISE NOTICE 'Perm check required but no user_id provided'; + RETURN summary; + END IF; + + IF NOT permission.usr_has_perm( + user_id, (SELECT code FROM permission.perm_list + WHERE id = org_setting_type.view_perm), org_id) + THEN + RAISE NOTICE 'Perm check failed for user % on %', + user_id, org_setting_type.view_perm; + RETURN summary; + END IF; + END IF; + + -- Perm check succeeded or was not necessary. + summary.value := setting_value; + RETURN summary; +END; +$FUNC$ LANGUAGE PLPGSQL; + +COMMIT; + + + -- 2.11.0