webstaff: Z39.50 search and import interface
authorGalen Charlton <gmc@esilibrary.com>
Tue, 9 Jun 2015 22:10:18 +0000 (22:10 +0000)
committerJason Stephenson <jstephenson@mvlc.org>
Wed, 19 Aug 2015 17:39:18 +0000 (13:39 -0400)
- fetch configured Z39.50 targets
- can now select targets
- search field form now active
- can now retrieve results
- check for empty query correctly
- retain reference to target list
- preserve search query input when toggling Z39.50 target selection
- implement clear form handler
- allow enter key to submit search
- track index of Z39.50 results so that individual ones can be selected
- note results of experiences to make the title be conditionally a hyperlink
- implement show in catalog
- implement direct import
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Signed-off-by: Jason Stephenson <jstephenson@mvlc.org>
Open-ILS/src/templates/staff/cat/z3950/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/z3950/t_list.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/z3950/t_search_fields.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/z3950/t_target.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/ui/default/staff/cat/services/z3950.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/cat/z3950/app.js [new file with mode: 0644]

diff --git a/Open-ILS/src/templates/staff/cat/z3950/index.tt2 b/Open-ILS/src/templates/staff/cat/z3950/index.tt2
new file mode 100644 (file)
index 0000000..02f8d08
--- /dev/null
@@ -0,0 +1,17 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Z39.50");
+  ctx.page_app = "egCatZ3950Search";
+  ctx.page_ctrl = "Z3950SearchCtrl";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/z3950/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/z3950.js"></script>
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_list.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_list.tt2
new file mode 100644 (file)
index 0000000..dcd5840
--- /dev/null
@@ -0,0 +1,56 @@
+<div class="row">
+    <div class="col-xs-6">
+        <strong>[% l('Query') %]</strong>
+        <form ng-keyup="$event.keyCode == 13 && search()">
+        <eg-z3950-search-field-list></eg-z3950-search-field-list>
+        </form>
+    </div>
+    <div class="col-xs-6">
+        <strong>[% l('Service and Credentials') %]</strong>
+        <eg-z3950-target-list>
+    </div>
+</div>
+
+<div class="row" id="z3950-search-form-row">
+    <form ng-submit="search()" id="z3950-search-form"
+        role="form" class="form-inline">
+        <div class="button-group">
+                <input type="submit" class="btn btn-primary" value="[% l('Search') %]"/>
+
+                <input type="reset" class="btn btn-primary" ng-click="clearForm()"
+                value="[% l('Clear Form') %]"/>
+        </div>
+    </form>
+</div>
+
+<eg-grid
+  id-field="index"
+  idl-class="mvr"
+  features="-display,-sort,-multisort"
+  main-label="[% l('Results') %]"
+  items-provider="z3950SearchGridProvider"
+  grid-controls="gridControls"
+  persist-key="cat.z3950_results">
+
+  <eg-grid-menu-item handler="showInCatalog" disabled="cant_showInCatalog"
+    label="[% l('Show in Catalog') %]"></eg-grid-menu>
+  <eg-grid-menu-item handler="import" disabled="cant_import"
+    label="[% l('Import') %]"></eg-grid-menu>
+
+    <!-- 
+      FIXME: it would be nice to make this column link
+      to record display page when the service is
+      'native-evergreen-catalog', but can't do ng-if
+      inside of column value templates at the moment
+    -->
+    <eg-grid-field label="[% l('Title') %]" path="title" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Author') %]" path="author" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Edition') %]" path="edition" visible></eg-grid-field>
+    <eg-grid-field label="[% l('ISBN') %]" path="isbn" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Publication Date') %]" path="pubdate" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Publisher') %]" path="publisher" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Service') %]" path="service" visible></eg-grid-field>
+    <eg-grid-field label="[% l('TCN') %]" path="tcn" visible></eg-grid-field>
+    <eg-grid-field label
+    <eg-grid-field path="*" hidden></eg-grid-field>
+</eg-grid>
diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_search_fields.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_search_fields.tt2
new file mode 100644 (file)
index 0000000..377695e
--- /dev/null
@@ -0,0 +1,8 @@
+<div class="form-horizontal">
+    <div ng-repeat="(code, search_field) in fields" class="z3950-search-field-list form-group">
+        <label for="z3950-field-{{code}}" class="col-xs-6 control-label">{{search_field.label}}</label>
+        <div class="col-xs-6">
+            <input type="text" class="form=control" id="z3950-field-{{code}}" ng-model="search_field.query">
+        </div>
+    </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_target.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_target.tt2
new file mode 100644 (file)
index 0000000..9365ec2
--- /dev/null
@@ -0,0 +1,19 @@
+<div ng-repeat="target in targets" class="z3950-target-list">
+    <div class="checkbox">
+        <input ng-model="target.selected" type="checkbox">
+    <div>
+    <div ng-if="target.code == 'native-evergreen-catalog'">[% l('Local Catalog') %]</div>
+    <div ng-if="target.code != 'native-evergreen-catalog'">{{target.settings.label}}</div>
+    <div ng-if="target.settings.auth == 't'" class="form-inline row">
+        <div class="form-group col-xs-6">
+            <label for="username-for-z3950-{{target.code}}">[% l('Username') %]</label>
+            <input type="text" class="form-control" id="username-for-z3950-{{target.code}}" ng-model="target.username">
+        </div>
+        <div class="form-group col-xs-6">
+            <label for="password-for-z3950-{{target.code}}">[% l('Password') %]</label>
+            <input type="text" class="form-control" id="password-for-z3950-{{target.code}}" ng-model="target.password">
+        </div>
+    </div>
+</div>
+    </div>
+</div>
index dd9cb38..6e2e8f7 100644 (file)
           </li>
           <li class="divider"></li>
           <li>
+            <a href="./cat/z3950/index" target="_self">
+              <span class="glyphicon glyphicon-cloud-download"></span>
+              [% l('Import Record from Z39.50') %]
+            </a>
+          </li>
+          <li>
             <a href="./cat/catalog/vandelay" target="_self">
               <span class="glyphicon glyphicon-transfer"></span>
               [% l('MARC Batch Import/Export') %]
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
new file mode 100644 (file)
index 0000000..4ef7e70
--- /dev/null
@@ -0,0 +1,132 @@
+angular.module('egZ3950Mod', ['egCoreMod', 'ui.bootstrap'])
+.factory('egZ3950TargetSvc',
+       ['$q', 'egCore', 'egAuth',
+function($q,   egCore,   egAuth) {
+    
+    var service = {
+        targets : [ ],
+        searchFields : { }
+    };
+    
+    service.loadTargets = function() {
+        egCore.net.request(
+            'open-ils.search',
+            'open-ils.search.z3950.retrieve_services',
+            egAuth.token()
+        ).then(function(res) {
+            // keep the reference, just clear the list
+            service.targets.length = 0;
+            // native Evergreen search goes first
+            var localTarget = res['native-evergreen-catalog'];
+            delete res['native-evergreen-catalog'];
+            angular.forEach(res, function(value, key) {
+                this.push({
+                    code:       key,
+                    settings:   value,
+                    selected:   false,
+                    username:   '',
+                    password:   ''
+                });
+            }, service.targets);
+            service.targets.sort(function (a, b) {
+                a = a.settings.label;
+                b = b.settings.label;
+                return a < b ? -1 : (a > b ? 1 : 0);
+            }); 
+            service.targets.unshift({
+                code:       'native-evergreen-catalog',
+                settings:   localTarget,
+                selected:   false,
+                username:   '',
+                password:   ''
+            });
+        });
+    };
+
+    service.loadActiveSearchFields = function() {
+        // don't want to throw away the reference, otherwise
+        // directives bound to searchFields won't
+        // refresh
+        var curFormInput = {};
+        for (var field in service.searchFields) {
+            curFormInput[field] = service.searchFields[field].query;
+            delete service.searchFields[field];
+        }
+        angular.forEach(service.targets, function(target, idx) {
+            if (target.selected) {
+                angular.forEach(target.settings.attrs, function(attr, key) {
+                    if (!(key in service.searchFields)) service.searchFields[key] = {
+                        label : attr.label,
+                        query : (key in curFormInput) ? curFormInput[key] : ''
+                    };
+                });
+            }
+        });
+    };
+
+    service.clearSearchFields = function() {
+        for (var field in service.searchFields) {
+            service.searchFields[field].query = '';
+        }
+    }
+
+    // return the selected Z39.50 targets and search strings
+    // in a format suitable for passing directly to
+    // open-ils.search.z3950.search_class
+    service.currentQuery = function() {
+        var query = {
+            service  : [],
+            username : [],
+            password : [],
+            search   : {}
+        };
+
+        angular.forEach(service.targets, function(target, idx) {
+            if (target.selected) {
+                query.service.push(target.code);
+                query.username.push(target.username);
+                query.password.push(target.password);
+            }
+        });
+        angular.forEach(service.searchFields, function(value, key) {
+            if (value.query && value.query.trim()) {
+                query.search[key] = value.query.trim();
+            }
+        });
+
+        return query;
+    }
+
+    return service;
+}])
+.directive("egZ3950TargetList", function () {
+    return {
+        transclude: true,
+        restrict:   'AE',
+        scope: {
+            
+        },
+        templateUrl: './cat/z3950/t_target',
+        controller:
+                   ['$scope','egZ3950TargetSvc',
+            function($scope , egZ3950TargetSvc) {
+                $scope.targets = egZ3950TargetSvc.targets;
+                $scope.$watch('targets', function(oldVal, newVal) {
+                    egZ3950TargetSvc.loadActiveSearchFields();
+                }, true);
+            }]
+    }
+})
+.directive("egZ3950SearchFieldList", ['egZ3950TargetSvc',
+    function(egZ3950TargetSvc) {
+        return {
+            restrict:   'AE',
+            scope: {
+            },
+            templateUrl: './cat/z3950/t_search_fields',
+            link: function(scope, elem, attr) {
+                scope.fields = egZ3950TargetSvc.searchFields;
+            }
+        };
+    }
+]);
diff --git a/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js b/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
new file mode 100644 (file)
index 0000000..8a6d7d6
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * Z39.50 search and import
+ */
+
+angular.module('egCatZ3950Search',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egZ3950Mod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+    // search page shows the list view by default
+    $routeProvider.when('/cat/z3950/search', {
+        templateUrl: './cat/z3950/t_list',
+        controller: 'Z3950SearchCtrl',
+        resolve : resolver
+    });
+
+    // default page / bucket view
+    $routeProvider.otherwise({redirectTo : '/cat/z3950/search'});
+})
+
+/**
+ * List view - grid stuff
+ */
+.controller('Z3950SearchCtrl',
+       ['$scope','$q','$location','$timeout','$window','egCore','egGridDataProvider','egZ3950TargetSvc',
+function($scope , $q , $location , $timeout , $window,  egCore , egGridDataProvider,  egZ3950TargetSvc ) {
+
+    // get list of targets
+    egZ3950TargetSvc.loadTargets();
+    egZ3950TargetSvc.loadActiveSearchFields();
+
+    var provider = egGridDataProvider.instance({});
+
+    provider.get = function(offset, count) {
+        var deferred = $q.defer();
+
+        var query = egZ3950TargetSvc.currentQuery();
+        if (Object.keys(query.search).length == 0) {
+            return $q.when();
+        }
+
+        query['limit'] = count;
+        query['offset'] = offset;
+
+        var resultIndex = offset;
+        egCore.net.request(
+            'open-ils.search',
+            'open-ils.search.z3950.search_class',
+            egCore.auth.token(),
+            query
+        ).then(
+            function() { deferred.resolve() },
+            null, // onerror
+            function(result) {
+                for (var i in result.records) {
+                    result.records[i].mvr['service'] = result.service;
+                    result.records[i].mvr['index'] = resultIndex++;
+                    result.records[i].mvr['marcxml'] = result.records[i].marcxml;
+                    deferred.notify(result.records[i].mvr);
+                }
+            }
+        );
+
+        return deferred.promise;
+    };
+
+    $scope.z3950SearchGridProvider = provider;
+    $scope.gridControls = {};
+
+    $scope.search = function() {
+        $scope.z3950SearchGridProvider.refresh();
+    };
+    $scope.clearForm = function() {
+        egZ3950TargetSvc.clearSearchFields();
+    };
+
+    $scope.showInCatalog = function() {
+        var items = $scope.gridControls.selectedItems();
+        // relying on cant_showInCatalog to protect us
+        var url = egCore.env.basePath +
+                  'cat/catalog/record/' + items[0].tcn();
+        $timeout(function() { $window.open(url, '_blank') });        
+    };
+    $scope.cant_showInCatalog = function() {
+        var items = $scope.gridControls.selectedItems();
+        if (items.length != 1) return true;
+        if (items[0]['service'] == 'native-evergreen-catalog') return false;
+        return true;
+    };
+
+    $scope.import = function() {
+        var deferred = $q.defer();
+        var items = $scope.gridControls.selectedItems();
+        egCore.net.request(
+            'open-ils.cat',
+            'open-ils.cat.biblio.record.xml.import',
+            egCore.auth.token(),
+            items[0]['marcxml']
+            // FIXME and more
+        ).then(
+            function() { deferred.resolve() },
+            null, // onerror
+            function(result) {
+                console.debug('imported');
+            }
+        );
+
+        return deferred.promise;
+    };
+    $scope.cant_import = function() {
+        var items = $scope.gridControls.selectedItems();
+        if (items.length == 1) return false;
+        return true;
+    };
+}])