LP1702929 Save grid settings as AOUS WIP user/berick/lp1702929-save-grid-settings-aous
authorBill Erickson <berickxx@gmail.com>
Tue, 29 Jan 2019 15:51:01 +0000 (10:51 -0500)
committerBill Erickson <berickxx@gmail.com>
Tue, 29 Jan 2019 15:51:01 +0000 (10:51 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
12 files changed:
Open-ILS/src/eg2/src/app/common.module.ts
Open-ILS/src/eg2/src/app/core/server-store.service.ts
Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-aous-org-select.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.setting-summary-perms.sql [new file with mode: 0644]

index c83ad39..0f37a0e 100644 (file)
@@ -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,
index 43415c1..1485b81 100644 (file)
@@ -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<any> {
         return this.setItem(key, null);
     }
+
+    getSettingSummary(key: string): Promise<ServerSettingSummary> {
+        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 (file)
index 0000000..0296ab1
--- /dev/null
@@ -0,0 +1,39 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Grid Configuration Library Setting</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body eg-grid-column-config-dialog">
+    <div class="row m-3">
+      <div class="col-lg-6">
+        <span>Apply settings to Org Unit:</span>
+      </div>
+      <div class="col-lg-6">
+        <!-- TODO -->
+        <eg-org-select></eg-org-select>
+        storeSettingsAsAousOrgs
+      </div>
+    </div>
+    <div class="row m-3">
+      <div class="col-lg-12">
+        <p i18n>
+          Applying values to setting: {{'eg.grid.' + context.persistKey}}.
+        </p>
+<pre>{{settingsAsString()}}</pre>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-warning ml-2" 
+      (click)="dismiss('canceled')" i18n>Cancel</button>
+    <button type="button" class="btn btn-success ml-2" 
+      (click)="save()" i18n>Save</button>
+  </div>
+</ng-template>
+
+
+
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 (file)
index 0000000..27ba368
--- /dev/null
@@ -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 [];
+        }
+    }
+    */
+}
+
+
index 5eaa81f..d97b1e8 100644 (file)
@@ -1,6 +1,9 @@
 
 <div class="eg-grid-toolbar mb-2">
 
+  <eg-grid-aous-org-select #aousOrgSelector [context]="gridContext">
+  </eg-grid-aous-org-select>
+
   <div class="btn-toolbar">
 
     <!-- buttons -->
         <span class="material-icons">save</span>
         <span class="ml-2" i18n>Save Grid Settings</span>
       </a>
+      <a *ngIf="gridContext.storeSettingsAsAousOrgs.length" 
+          class="dropdown-item label-with-material-icon" 
+        (click)="aousOrgSelector.open()">
+        <span class="material-icons">save</span>
+        <span class="ml-2" i18n>Save Grid Settings As Library Setting</span>
+      </a>
+
       <a class="dropdown-item label-with-material-icon" 
         (click)="gridContext.columnSet.reset()">
         <span class="material-icons">restore</span>
index 399a4c7..60ac425 100644 (file)
@@ -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());
     }
index 1fa4c2c..52b82ab 100644 (file)
@@ -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<any>();
         this.onRowClick = new EventEmitter<any>();
     }
index 0773a7e..24b7184 100644 (file)
@@ -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
index 8a777e4..20a62b9 100644 (file)
@@ -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<GridPersistConf> {
         if (!persistKey) { return Promise.resolve(null); }
         return this.store.getItem('eg.grid.' + persistKey);
index e83143c..3fafb71 100644 (file)
@@ -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,
index c387967..e18abd4 100644 (file)
@@ -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 (file)
index 0000000..cff71d9
--- /dev/null
@@ -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;
+
+
+