7ad50b3e31f24bb8bf9b077a58a77bd4ce893b1a
[Evergreen.git] / Open-ILS / web / vandelay / vandelay.js
1 /* ---------------------------------------------------------------------------
2 # Copyright (C) 2008  Georgia Public Library Service
3 # Bill Erickson <erickson@esilibrary.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # --------------------------------------------------------------------------- */
15 dojo.require("dojo.parser");
16 dojo.require("dojo.io.iframe"); 
17 dojo.require("dijit.ProgressBar"); 
18 dojo.require("dijit.form.Button"); 
19 dojo.require("dijit.form.FilteringSelect"); 
20 dojo.require("dijit.layout.ContentPane");
21 dojo.require("dijit.layout.TabContainer");
22 dojo.require("dijit.layout.LayoutContainer");
23 dojo.require('dijit.form.Button');
24 dojo.require('dijit.Toolbar');
25 dojo.require('dijit.Tooltip');
26 dojo.require('dijit.Menu');
27 dojo.require("dijit.Dialog");
28 dojo.require("dojo.cookie");
29 dojo.require("dojox.grid.Grid");
30 dojo.require("dojo.data.ItemFileReadStore");
31 dojo.require('dojo.date.locale');
32 dojo.require('dojo.date.stamp');
33 dojo.require("fieldmapper.Fieldmapper");
34 dojo.require("fieldmapper.dojoData");
35 dojo.require('openils.CGI');
36 dojo.require('openils.User');
37 dojo.require('openils.Event');
38 dojo.require('openils.MarcXPathParser');
39 dojo.require('openils.GridColumnPicker');
40
41
42 var globalDivs = [
43     'vl-generic-progress',
44     'vl-generic-progress-with-total',
45     'vl-marc-upload-div',
46     'vl-queue-div',
47     'vl-match-div',
48     'vl-marc-html-div',
49     'vl-queue-select-div',
50     'vl-marc-upload-status-div',
51     'vl-attr-editor-div',
52 ];
53
54 var authtoken;
55 var VANDELAY_URL = '/vandelay-upload';
56 var bibAttrDefs = [];
57 var authAttrDefs = [];
58 var queuedRecords = [];
59 var queuedRecordsMap = {};
60 var bibAttrsFetched = false;
61 var authAttrsFetched = false;
62 var attrDefMap = {}; // maps attr def code names to attr def ids
63 var currentType;
64 var currentQueueId = null;
65 var userCache = {};
66 var currentMatchedRecords; // set of loaded matched bib records
67 var currentOverlayRecordsMap; // map of import record to overlay record
68 var currentImportRecId; // when analyzing matches, this is the current import record
69 var userBibQueues = []; // only non-complete queues
70 var userAuthQueues = []; // only non-complete queues
71 var allUserBibQueues;
72 var allUserAuthQueues;
73 var selectableGridRecords;
74 var cgi = new openils.CGI();
75 var vlQueueGridColumePicker;
76
77 /**
78   * Grab initial data
79   */
80 function vlInit() {
81     authtoken = dojo.cookie('ses') || cgi.param('ses');
82     var initNeeded = 4; // how many async responses do we need before we're init'd 
83     var initCount = 0; // how many async reponses we've received
84
85     function checkInitDone() {
86         initCount++;
87         if(initCount == initNeeded)
88             runStartupCommands();
89     }
90
91     // Fetch the bib and authority attribute definitions 
92     vlFetchBibAttrDefs(function () { checkInitDone(); });
93     vlFetchAuthAttrDefs(function () { checkInitDone(); });
94
95     vlRetrieveQueueList('bib', null, 
96         function(list) {
97             allUserBibQueues = list;
98             for(var i = 0; i < allUserBibQueues.length; i++) {
99                 if(allUserBibQueues[i].complete() == 'f')
100                     userBibQueues.push(allUserBibQueues[i]);
101             }
102             checkInitDone();
103         }
104     );
105
106     vlRetrieveQueueList('auth', null, 
107         function(list) {
108             allUserAuthQueues = list;
109             for(var i = 0; i < allUserAuthQueues.length; i++) {
110                 if(allUserAuthQueues[i].complete() == 'f')
111                     userAuthQueues.push(allUserAuthQueues[i]);
112             }
113             checkInitDone();
114         }
115     );
116
117     vlAttrEditorInit();
118 }
119
120
121 dojo.addOnLoad(vlInit);
122
123
124 // fetch the bib and authority attribute definitions
125
126 function vlFetchBibAttrDefs(postcomplete) {
127     bibAttrDefs = [];
128     fieldmapper.standardRequest(
129         ['open-ils.permacrud', 'open-ils.permacrud.search.vqbrad'],
130         {   async: true,
131             params: [authtoken, {id:{'!=':null}}],
132             onresponse: function(r) {
133                 var def = r.recv().content(); 
134                 if(e = openils.Event.parse(def[0])) 
135                     return alert(e);
136                 bibAttrDefs.push(def);
137             },
138             oncomplete: function() {
139                 bibAttrDefs = bibAttrDefs.sort(
140                     function(a, b) {
141                         if(a.id() > b.id()) return 1;
142                         if(a.id() < b.id()) return -1;
143                         return 0;
144                     }
145                 );
146                 postcomplete();
147             }
148         }
149     );
150 }
151
152 function vlFetchAuthAttrDefs(postcomplete) {
153     authAttrDefs = [];
154     fieldmapper.standardRequest(
155         ['open-ils.permacrud', 'open-ils.permacrud.search.vqarad'],
156         {   async: true,
157             params: [authtoken, {id:{'!=':null}}],
158             onresponse: function(r) {
159                 var def = r.recv().content(); 
160                 if(e = openils.Event.parse(def[0])) 
161                     return alert(e);
162                 authAttrDefs.push(def);
163             },
164             oncomplete: function() {
165                 authAttrDefs = authAttrDefs.sort(
166                     function(a, b) {
167                         if(a.id() > b.id()) return 1;
168                         if(a.id() < b.id()) return -1;
169                         return 0;
170                     }
171                 );
172                 postcomplete();
173             }
174         }
175     );
176 }
177
178 function vlRetrieveQueueList(type, filter, onload) {
179     type = (type == 'bib') ? type : 'authority';
180     fieldmapper.standardRequest(
181         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.owner.retrieve.atomic'],
182         {   async: true,
183             params: [authtoken, null, filter],
184             oncomplete: function(r) {
185                 var list = r.recv().content();
186                 if(e = openils.Event.parse(list[0]))
187                     return alert(e);
188                 onload(list);
189             }
190         }
191     );
192
193 }
194
195 function displayGlobalDiv(id) {
196     for(var i = 0; i < globalDivs.length; i++) {
197         try {
198             dojo.style(dojo.byId(globalDivs[i]), 'display', 'none');
199         } catch(e) {
200             alert('please define div ' + globalDivs[i]);
201         }
202     }
203     dojo.style(dojo.byId(id),'display','block');
204 }
205
206 function runStartupCommands() {
207     currentQueueId = cgi.param('qid');
208     currentType = cgi.param('qtype');
209     dojo.style('vl-nav-bar', 'visibility', 'visible');
210     if(currentQueueId)
211         return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
212     vlShowUploadForm();
213 }
214
215 /**
216   * asynchronously upload a file of MARC records
217   */
218 function uploadMARC(onload){
219     dojo.byId('vl-upload-status-count').innerHTML = '0';
220     dojo.byId('vl-ses-input').value = authtoken;
221     displayGlobalDiv('vl-marc-upload-status-div');
222     dojo.io.iframe.send({
223         url: VANDELAY_URL,
224         method: "post",
225         handleAs: "html",
226         form: dojo.byId('vl-marc-upload-form'),
227         handle: function(data,ioArgs){
228             var content = data.documentElement.textContent;
229             onload(content);
230         }
231     });
232 }       
233
234 /**
235   * Creates a new vandelay queue
236   */
237 function createQueue(queueName, type, onload) {
238     fieldmapper.standardRequest(
239         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.create'],
240         {   async: true,
241             params: [authtoken, queueName, null, type],
242             oncomplete : function(r) {
243                 var queue = r.recv().content();
244                 if(e = openils.Event.parse(queue)) 
245                     return alert(e);
246                 onload(queue);
247             }
248         }
249     );
250 }
251
252 /**
253   * Tells vandelay to pull a batch of records from the cache and explode them
254   * out into the vandelay tables
255   */
256 function processSpool(key, queueId, type, onload) {
257     fieldmapper.standardRequest(
258         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'.process_spool'],
259         {   async: true,
260             params: [authtoken, key, queueId],
261             onresponse : function(r) {
262                 var resp = r.recv().content();
263                 if(e = openils.Event.parse(resp)) 
264                     return alert(e);
265                 dojo.byId('vl-upload-status-count').innerHTML = resp;
266             },
267             oncomplete : function(r) {onload();}
268         }
269     );
270 }
271
272 function retrieveQueuedRecords(type, queueId, onload) {
273     displayGlobalDiv('vl-generic-progress');
274     queuedRecords = [];
275     queuedRecordsMap = {};
276     currentOverlayRecordsMap = {};
277     selectableGridRecords = {};
278     resetVlQueueGridLayout();
279
280     var method = 'open-ils.vandelay.'+type+'_queue.records.retrieve.atomic';
281     if(vlQueueGridShowMatches.checked)
282         method = method.replace('records', 'records.matches');
283
284     var limit = parseInt(vlQueueDisplayLimit.getValue());
285     var offset = limit * parseInt(vlQueueDisplayPage.getValue()-1);
286
287     fieldmapper.standardRequest(
288         ['open-ils.vandelay', method],
289         {   async: true,
290             params: [authtoken, queueId, 
291                 {   clear_marc: 1, 
292                     offset: offset,
293                     limit: limit
294                 }
295             ],
296             /* intermittent bug in streaming, multipart requests prevents use of onreponse for now...
297             onresponse: function(r) {
298                 var rec = r.recv().content();
299                 if(e = openils.Event.parse(rec))
300                     return alert(e);
301                 queuedRecords.push(rec);
302                 queuedRecordsMap[rec.id()] = rec;
303             },
304             */
305             oncomplete: function(r){
306                 var recs = r.recv().content();
307                 if(e = openils.Event.parse(recs[0]))
308                     return alert(e);
309                 for(var i = 0; i < recs.length; i++) {
310                     var rec = recs[i];
311                     queuedRecords.push(rec);
312                     queuedRecordsMap[rec.id()] = rec;
313                 }
314                 onload();
315             }
316         }
317     );
318 }
319
320 //function vlLoadMatchUI(recId, attrCode) {
321 function vlLoadMatchUI(recId) {
322     displayGlobalDiv('vl-generic-progress');
323     //var matches = getRecMatchesFromAttrCode(queuedRecordsMap[recId], attrCode);
324     var matches = queuedRecordsMap[recId].matches();
325     var records = [];
326     currentImportRecId = recId;
327     for(var i = 0; i < matches.length; i++)
328         records.push(matches[i].eg_record());
329
330     var retrieve = ['open-ils.search', 'open-ils.search.biblio.record_entry.slim.retrieve'];
331     var params = [records];
332     if(currentType == 'auth') {
333         retrieve = ['open-ils.cat', 'open-ils.cat.authority.record.retrieve'];
334         parmas = [authtoken, records, {clear_marc:1}];
335     }
336
337     fieldmapper.standardRequest(
338         retrieve,
339         {   async: true,
340             params:params,
341             oncomplete: function(r) {
342                 var recs = r.recv().content();
343                 if(e = openils.Event.parse(recs))
344                     return alert(e);
345
346                 /* ui mangling */
347                 displayGlobalDiv('vl-match-div');
348                 resetVlMatchGridLayout();
349                 currentMatchedRecords = recs;
350                 vlMatchGrid.setStructure(vlMatchGridLayout);
351
352                 // build the data store of records with match information
353                 var dataStore = bre.toStoreData(recs, null, 
354                     {virtualFields:['dest_matchpoint', 'src_matchpoint', '_id']});
355                 dataStore.identifier = '_id';
356
357
358                 for(var i = 0; i < dataStore.items.length; i++) {
359                     var item = dataStore.items[i];
360                     item._id = i; // just need something unique
361                     for(var j = 0; j < matches.length; j++) {
362                         var match = matches[j];
363                         if(match.eg_record() == item.id) {
364                             item.dest_matchpoint = match.field_type();
365                             var attr = getRecAttrFromMatch(queuedRecordsMap[recId], match);
366                             //item.src_matchpoint = getRecAttrDefFromAttr(attr, currentType).description();
367                             item.src_matchpoint = getRecAttrDefFromAttr(attr, currentType).code();
368                         }
369                     }
370                 }
371
372                 // now populate the grid
373                 vlPopulateMatchGrid(vlMatchGrid, dataStore);
374             }
375         }
376     );
377 }
378
379 function vlPopulateMatchGrid(grid, data) {
380     var store = new dojo.data.ItemFileReadStore({data:data});
381     var model = new dojox.grid.data.DojoData(
382         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
383     grid.setModel(model);
384     grid.update();
385 }
386
387 function showMe(id) {
388     dojo.style(dojo.byId(id), 'display', 'block');
389 }
390 function hideMe(id) {
391     dojo.style(dojo.byId(id), 'display', 'none');
392 }
393
394
395 function vlLoadMARCHtml(recId, inCat, oncomplete) {
396     dijit.byId('vl-marc-html-done-button').onClick = oncomplete;
397     displayGlobalDiv('vl-generic-progress');
398     var api;
399     var params = [recId, 1];
400
401     if(inCat) {
402         hideMe('vl-marc-html-edit-button'); // don't show marc editor button
403         dijit.byId('vl-marc-html-edit-button').onClick = function(){}
404         api = ['open-ils.search', 'open-ils.search.biblio.record.html'];
405         if(currentType == 'auth')
406             api = ['open-ils.search', 'open-ils.search.authority.to_html'];
407     } else {
408         showMe('vl-marc-html-edit-button'); // plug in the marc editor button
409         dijit.byId('vl-marc-html-edit-button').onClick = function() {vlLoadMarcEditor(currentType, recId);};
410         params = [authtoken, recId];
411         api = ['open-ils.vandelay', 'open-ils.vandelay.queued_bib_record.html'];
412         if(currentType == 'auth')
413             api = ['open-ils.vandelay', 'open-ils.vandelay.queued_authority_record.html'];
414     }
415
416     fieldmapper.standardRequest(
417         api, 
418         {   async: true,
419             params: params,
420             oncomplete: function(r) {
421             displayGlobalDiv('vl-marc-html-div');
422                 var html = r.recv().content();
423                 dojo.byId('vl-marc-record-html').innerHTML = html;
424             }
425         }
426     );
427 }
428
429
430 /*
431 function getRecMatchesFromAttrCode(rec, attrCode) {
432     var matches = [];
433     var attr = getRecAttrFromCode(rec, attrCode);
434     for(var j = 0; j < rec.matches().length; j++) {
435         var match = rec.matches()[j];
436         if(match.matched_attr() == attr.id()) 
437             matches.push(match);
438     }
439     return matches;
440 }
441 */
442
443 function getRecAttrFromMatch(rec, match) {
444     for(var i = 0; i < rec.attributes().length; i++) {
445         var attr = rec.attributes()[i];
446         if(attr.id() == match.matched_attr())
447             return attr;
448     }
449 }
450
451 function getRecAttrDefFromAttr(attr, type) {
452     var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
453     for(var i = 0; i < defs.length; i++) {
454         var def = defs[i];
455         if(def.id() == attr.field())
456             return def;
457     }
458 }
459
460 function getRecAttrFromCode(rec, attrCode) {
461     var defId = attrDefMap[attrCode];
462     var attrs = rec.attributes();
463     for(var i = 0; i < attrs.length; i++) {
464         var attr = attrs[i];
465         if(attr.field() == defId) 
466             return attr;
467     }
468     return null;
469 }
470
471 function vlGetViewMatches(rowIdx) {
472     var data = this.grid.model.getRow(rowIdx);
473     if(!data) return '';
474     var rec = queuedRecordsMap[data.id];
475     if(rec.matches().length > 0)
476         return this.value.replace('RECID', data.id);
477     return '';
478 }
479
480 function getAttrValue(rowIdx) {
481     var data = this.grid.model.getRow(rowIdx);
482     if(!data) return '';
483     var attrCode = this.field.split('.')[1];
484     var rec = queuedRecordsMap[data.id];
485     var attr = getRecAttrFromCode(rec, attrCode);
486     return (attr) ? attr.attr_value() : '';
487 }
488
489 function vlGetDateTimeField(rowIdx) {
490     data = this.grid.model.getRow(rowIdx);
491     if(!data) return '';
492     if(!data[this.field]) return '';
493     var date = dojo.date.stamp.fromISOString(data[this.field]);
494     return dojo.date.locale.format(date, {selector:'date'});
495 }
496
497 function vlGetCreator(rowIdx) {
498     data = this.grid.model.getRow(rowIdx);
499     if(!data) return '';
500     var id = data.creator;
501     if(userCache[id])
502         return userCache[id].usrname();
503     var user = fieldmapper.standardRequest(
504         ['open-ils.actor', 'open-ils.actor.user.retrieve'], [authtoken, id]);
505     if(e = openils.Event.parse(user))
506         return alert(e);
507     userCache[id] = user;
508     return user.usrname();
509 }
510
511 function vlGetViewMARC(rowIdx) {
512     data = this.grid.model.getRow(rowIdx);
513     if(data) 
514         return this.value.replace('RECID', data.id);
515 }
516
517 function vlGetOverlayTargetSelector(rowIdx) {
518     data = this.grid.model.getRow(rowIdx);
519     if(data) {
520         var value = this.value.replace('ID', data.id);
521         var overlay = currentOverlayRecordsMap[currentImportRecId];
522         if(overlay && overlay == data.id) 
523             value = value.replace('/>', 'checked="checked"/>');
524         return value;
525     }
526 }
527
528 /**
529   * see if the user has enabled overlays for the current match set and, 
530   * if so, map the current import record to the overlay target.
531   */
532 function vlHandleOverlayTargetSelected() {
533     if(vlOverlayTargetEnable.checked) {
534         for(var i = 0; i < currentMatchedRecords.length; i++) {
535             var matchRecId = currentMatchedRecords[i].id();
536             if(dojo.byId('vl-overlay-target-'+matchRecId).checked) {
537                 console.log("found overlay target " + matchRecId);
538                 currentOverlayRecordsMap[currentImportRecId] = matchRecId;
539                 dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = true;
540                 dojo.byId('vl-record-list-selected-' + currentImportRecId).parentNode.className = 'overlay_selected';
541                 return;
542             }
543         }
544     } else {
545         delete currentOverlayRecordsMap[currentImportRecId];
546         dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = false;
547     }
548 }
549
550 var vlQueueGridBuilt = false;
551 function buildRecordGrid(type) {
552     displayGlobalDiv('vl-queue-div');
553
554     currentOverlayRecordsMap = {};
555
556     if(!vlQueueGridBuilt) {
557         var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
558         for(var i = 0; i < defs.length; i++) {
559             var def = defs[i]
560             attrDefMap[def.code()] = def.id();
561             var col = {
562                 name:def.description(), 
563                 field:'attr.' + def.code(),
564                 get: getAttrValue,
565                 selectableColumn:true
566             };
567             vlQueueGridLayout[0].cells[0].push(col);
568         }
569         vlQueueGridBuilt = true;
570     }
571
572     var storeData;
573     if(type == 'bib')
574         storeData = vqbr.toStoreData(queuedRecords);
575     else
576         storeData = vqar.toStoreData(queuedRecords);
577
578     var store = new dojo.data.ItemFileReadStore({data:storeData});
579     var model = new dojox.grid.data.DojoData(
580         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
581     vlQueueGrid.setModel(model);
582
583     if(vlQueueGridColumePicker) {
584         vlQueueGrid.update();
585     } else {
586         vlQueueGridColumePicker = 
587             new openils.GridColumnPicker(vlQueueGridColumePickerDialog, 
588                 vlQueueGrid, vlQueueGridLayout, authtoken, 'vandelay.queue');
589         vlQueueGridColumePicker.load();
590     }
591 }
592
593 function vlQueueGridPrevPage() {
594     var page = parseInt(vlQueueDisplayPage.getValue());
595     if(page < 2) return;
596     vlQueueDisplayPage.setValue(page - 1);
597     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
598 }
599
600 function vlQueueGridNextPage() {
601     vlQueueDisplayPage.setValue(parseInt(vlQueueDisplayPage.getValue())+1);
602     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
603 }
604
605 function vlDeleteQueue(type, queueId, onload) {
606     fieldmapper.standardRequest(
607         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.delete'],
608         {   async: true,
609             params: [authtoken, queueId],
610             oncomplete: function(r) {
611                 var resp = r.recv().content();
612                 if(e = openils.Event.parse(resp))
613                     return alert(e);
614                 onload();
615             }
616         }
617     );
618 }
619
620
621 function vlQueueGridDrawSelectBox(rowIdx) {
622     var data = this.grid.model.getRow(rowIdx);
623     if(!data) return '';
624     var domId = 'vl-record-list-selected-' +data.id;
625     selectableGridRecords[domId] = data.id;
626     return "<div><input type='checkbox' id='"+domId+"'/></div>";
627 }
628
629 function vlSelectAllQueueGridRecords() {
630     for(var id in selectableGridRecords) 
631         dojo.byId(id).checked = true;
632 }
633 function vlSelectNoQueueGridRecords() {
634     for(var id in selectableGridRecords) 
635         dojo.byId(id).checked = false;
636 }
637 function vlToggleQueueGridSelect() {
638     if(dojo.byId('vl-queue-grid-row-selector').checked)
639         vlSelectAllQueueGridRecords();
640     else
641         vlSelectNoQueueGridRecords();
642 }
643
644 var handleRetrieveRecords = function() {
645     buildRecordGrid(currentType);
646 }
647
648 function vlImportSelectedRecords() {
649     displayGlobalDiv('vl-generic-progress-with-total');
650     var records = [];
651
652     for(var id in selectableGridRecords) {
653         if(dojo.byId(id).checked) {
654             var recId = selectableGridRecords[id];
655             var rec = queuedRecordsMap[recId];
656             if(!rec.import_time()) 
657                 records.push(recId);
658         }
659     }
660
661     fieldmapper.standardRequest(
662         ['open-ils.vandelay', 'open-ils.vandelay.'+currentType+'_record.list.import'],
663         {   async: true,
664             params: [authtoken, records, {overlay_map:currentOverlayRecordsMap}],
665             onresponse: function(r) {
666                 var resp = r.recv().content();
667                 if(e = openils.Event.parse(resp))
668                     return alert(e);
669                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
670             },
671             oncomplete: function() {
672                 return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
673             }
674         }
675     );
676 }
677
678 function vlImportRecordQueue(type, queueId, noMatchOnly, onload) {
679     displayGlobalDiv('vl-generic-progress-with-total');
680     var method = 'open-ils.vandelay.bib_queue.import';
681     if(noMatchOnly)
682         method = method.replace('import', 'nomatch.import');
683     if(type == 'auth')
684         method = method.replace('bib', 'auth');
685
686     fieldmapper.standardRequest(
687         ['open-ils.vandelay', method],
688         {   async: true,
689             params: [authtoken, queueId],
690             onresponse: function(r) {
691                 var resp = r.recv().content();
692                 if(e = openils.Event.parse(resp))
693                     return alert(e);
694                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
695             },
696             oncomplete: function() {onload();}
697         }
698     );
699 }
700
701
702 /**
703   * Create queue, upload MARC, process spool, load the newly created queue 
704   */
705 function batchUpload() {
706     var queueName = dijit.byId('vl-queue-name').getValue();
707     currentType = dijit.byId('vl-record-type').getValue();
708
709     var handleProcessSpool = function() {
710         console.log('records uploaded and spooled');
711         if(vlUploadQueueAutoImport.checked) {
712             vlImportRecordQueue(currentType, currentQueueId, true,  
713                 function() {
714                     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
715                 }
716             );
717         } else {
718             retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
719         }
720     }
721
722     var handleUploadMARC = function(key) {
723         console.log('marc uploaded');
724         dojo.style(dojo.byId('vl-upload-status-processing'), 'display', 'block');
725         processSpool(key, currentQueueId, currentType, handleProcessSpool);
726     };
727
728     var handleCreateQueue = function(queue) {
729         console.log('queue created ' + queue.name());
730         currentQueueId = queue.id();
731         uploadMARC(handleUploadMARC);
732     };
733     
734     if(vlUploadQueueSelector.getValue() && !queueName) {
735         currentQueueId = vlUploadQueueSelector.getValue();
736         console.log('adding records to existing queue ' + currentQueueId);
737         uploadMARC(handleUploadMARC);
738     } else {
739         createQueue(queueName, currentType, handleCreateQueue);
740     }
741 }
742
743
744 function vlFleshQueueSelect(selector, type) {
745     var data = (type == 'bib') ? vbq.toStoreData(allUserBibQueues) : vaq.toStoreData(allUserAuthQueues);
746     selector.store = new dojo.data.ItemFileReadStore({data:data});
747     selector.setValue(null);
748     selector.setDisplayedValue('');
749     if(data[0])
750         selector.setValue(data[0].id());
751 }
752
753 function vlShowUploadForm() {
754     displayGlobalDiv('vl-marc-upload-div');
755     vlFleshQueueSelect(vlUploadQueueSelector, vlUploadRecordType.getValue());
756 }
757
758 function vlShowQueueSelect() {
759     displayGlobalDiv('vl-queue-select-div');
760     vlFleshQueueSelect(vlQueueSelectQueueList, vlQueueSelectType.getValue());
761 }
762
763 function vlFetchQueueFromForm() {
764     currentType = vlQueueSelectType.getValue();
765     currentQueueId = vlQueueSelectQueueList.getValue();
766     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
767 }
768
769 function vlOpenMarcEditWindow(rec) {
770     /*
771         To run in Firefox directly, must set signed.applets.codebase_principal_support
772         to true in about:config
773     */
774     netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
775     win = window.open('/xul/server/cat/marcedit.xul'); // XXX version?
776     win.xulG = {
777         record : {marc : rec.marc()},
778         save : {
779             label: 'Save', // XXX
780             func: function(xmlString) {
781                 var method = 'open-ils.permacrud.update.' + rec.classname;
782                 rec.marc(xmlString);
783                 fieldmapper.standardRequest(
784                     ['open-ils.permacrud', method],
785                     {   async: true,
786                         params: [authtoken, rec],
787                         oncomplete: function(r) {
788                             if(e = openils.Event.parse(rec))
789                                 return alert(e);
790                             alert('Record Updated'); // XXX
791                             win.close();
792                             // XXX reload marc html view with updates
793                         }
794                     }
795                 );
796             },
797         }
798     };
799 }
800
801 function vlLoadMarcEditor(type, recId) {
802     var method = 'open-ils.permacrud.search.vqbr';
803     if(currentType != 'bib')
804         method = method.replace(/vqbr/,'vqar');
805
806     fieldmapper.standardRequest(
807         ['open-ils.permacrud', method],
808         {   async: true, 
809             params: [authtoken, {id : recId}],
810             oncomplete: function(r) {
811                 var rec = r.recv().content();
812                 if(e = openils.Event.parse(rec))
813                     return alert(e);
814                 vlOpenMarcEditWindow(rec);
815             }
816         }
817     );
818 }
819
820
821
822 //------------------------------------------------------------
823 // attribute editors
824
825 // attribute-editor global variables
826
827 var ATTR_EDITOR_IN_UPDATE_MODE = false; // true on 'edit', false on 'create'
828 var ATTR_EDIT_ID = null;                // id of current 'edit' attribute
829 var ATTR_EDIT_GROUP = 'bib';            // bib-attrs or auth-attrs
830
831 function vlAttrEditorInit() {
832     // set up tooltips on the edit form
833     connectTooltip('attr-editor-tags'); 
834     connectTooltip('attr-editor-subfields'); 
835 }
836
837 function vlShowAttrEditor() {
838     displayGlobalDiv('vl-attr-editor-div');
839     loadAttrEditorGrid();
840     idHide('vl-generic-progress');
841 }
842
843 function setAttrEditorGroup(groupName) {
844     // put us into 'bib'-attr or 'auth'-attr mode.
845     if (ATTR_EDIT_GROUP != groupName) {
846         ATTR_EDIT_GROUP = groupName;
847         loadAttrEditorGrid();
848     }
849 }
850
851 function onAttrEditorOpen() {
852     // the "bars" have the create/update/cancel/etc. buttons.
853     var create_bar = document.getElementById('attr-editor-create-bar');
854     var update_bar = document.getElementById('attr-editor-update-bar');
855     if (ATTR_EDITOR_IN_UPDATE_MODE) {
856         update_bar.style.display='table-row';
857         create_bar.style.display='none';
858         // hide the dropdown-button
859         idStyle('vl-create-attr-editor-button', 'visibility', 'hidden');
860     } else {
861         dijit.byId('attr-editor-dialog').reset();
862         create_bar.style.display='table-row';
863         update_bar.style.display='none';
864     }
865 }
866
867 function onAttrEditorClose() {
868     // reset the form to a "create" form. (We may have borrowed it for editing.)
869     ATTR_EDITOR_IN_UPDATE_MODE = false;
870     // show the dropdown-button
871     idStyle('vl-create-attr-editor-button', 'visibility', 'visible');
872 }
873
874 function loadAttrEditorGrid() {
875     var _data = (ATTR_EDIT_GROUP == 'auth') ? 
876         vqarad.toStoreData(authAttrDefs) : vqbrad.toStoreData(bibAttrDefs) ;
877                  
878     var store = new dojo.data.ItemFileReadStore({data:_data});
879     var model = new dojox.grid.data.DojoData(
880         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
881     attrEditorGrid.setModel(model);
882     attrEditorGrid.setStructure(vlAttrGridLayout);
883     attrEditorGrid.onRowClick = onAttrEditorClick;
884     attrEditorGrid.update();
885 }
886
887 function attrGridGetTag(n) {
888     // grid helper: return the tags from the row's xpath column.
889     var xp = this.grid.model.getRow(n);
890     return xp && xpathParser.parse(xp.xpath).tags;
891 }
892
893 function attrGridGetSubfield(n) {
894     // grid helper: return the subfields from the row's xpath column.
895     var xp = this.grid.model.getRow(n);
896     return xp && xpathParser.parse(xp.xpath).subfields;
897 }
898
899 function onAttrEditorClick(evt) {
900     var row = attrEditorGrid.model.getRow(evt.rowIndex);
901     ATTR_EDIT_ID = row.id;
902     ATTR_EDITOR_IN_UPDATE_MODE = true;
903
904     // populate the popup editor.
905     dojo.byId('attr-editor-code').value = row.code;
906     dojo.byId('attr-editor-description').value = row.description;
907     var parsed_xpath = xpathParser.parse(row.xpath);
908     dojo.byId('attr-editor-tags').value = parsed_xpath.tags;
909     dojo.byId('attr-editor-subfields').value = parsed_xpath.subfields;
910     dojo.byId('attr-editor-identifier').value = (row.ident ? 'True':'False');
911     dojo.byId('attr-editor-xpath').value = row.xpath;
912     dojo.byId('attr-editor-remove').value = row.remove;
913
914     // set up UI for editing
915     dojo.byId('vl-create-attr-editor-button').click();
916 }
917
918 function vlSaveAttrDefinition(data) {
919     idHide('vl-attr-editor-div');
920     idShow('vl-generic-progress');
921
922     data.id = ATTR_EDIT_ID;
923
924     // this ought to honour custom xpaths, but overwrite xpaths
925     // derived from tags/subfields.
926     if (data.xpath == '' || looksLikeDerivedXpath(data.xpath)) {
927         var _xpath = tagAndSubFieldsToXpath(data.tag, data.subfield);
928         data.xpath = _xpath;
929     }
930
931     // build up our permacrud params. Key variables here are
932     // "create or update" and "bib or auth".
933
934     var isAuth   = (ATTR_EDIT_GROUP == 'auth');
935     var isCreate = (ATTR_EDIT_ID == null);
936     var rad      = isAuth ? new vqarad() : new vqbrad() ;
937     var method   = 'open-ils.permacrud' + (isCreate ? '.create.' : '.update.') 
938         + (isAuth ? 'vqarad' : 'vqbrad');
939     var _data    = rad.fromStoreItem(data);
940
941     _data.ischanged(1);
942
943     fieldmapper.standardRequest(
944         ['open-ils.permacrud', method],
945         {   async: true,
946             params: [authtoken, _data ],
947             onresponse: function(r) { },
948             oncomplete: function(r) {
949                 attrEditorFetchAttrDefs(vlShowAttrEditor);
950                 ATTR_EDIT_ID = null;
951             },
952             onerror: function(r) {
953                 alert('vlSaveAttrDefinition comms error: ' + r);
954             }
955         }
956     );
957 }
958
959 function attrEditorFetchAttrDefs(callback) {
960     var fn = (ATTR_EDIT_GROUP == 'auth') ? vlFetchAuthAttrDefs : vlFetchBibAttrDefs;
961     return fn(callback);
962 }
963
964 function vlAttrDelete() {
965     idHide('vl-attr-editor-div');
966     idShow('vl-generic-progress');
967
968     var isAuth = (ATTR_EDIT_GROUP == 'auth');
969     var method = 'open-ils.permacrud.delete.' + (isAuth ? 'vqarad' : 'vqbrad');
970     var rad    = isAuth ? new vqarad() : new vqbrad() ;
971     fieldmapper.standardRequest(
972         ['open-ils.permacrud', method],
973         {   async: true,
974             params: [authtoken, rad.fromHash({ id : ATTR_EDIT_ID }), ],
975             oncomplete: function() {
976                 dijit.byId('attr-editor-dialog').onCancel(); // close the dialog
977                 attrEditorFetchAttrDefs(vlShowAttrEditor);
978                 ATTR_EDIT_ID = null;
979             },
980             onerror: function(r) {
981                 alert('vlAttrDelete comms error: ' + r);
982             }
983         }
984     );
985 }
986
987 // ------------------------------------------------------------
988 // utilities for attribute editors
989
990 // dom utilities (maybe dojo does these, and these should be replaced)
991
992 function idStyle(obId, k, v)    { document.getElementById(obId).style[k] = v;   }
993 function idShow(obId)           { idStyle(obId, 'display', 'block');            }
994 function idHide(obId)           { idStyle(obId, 'display' , 'none');            }
995
996 function connectTooltip(fieldId) {
997     // Given an element id, look up a tooltip element in the doc (same
998     // id with a '-tip' suffix) and associate the two. Maybe dojo has
999     // a better way to do this?
1000     var fld = dojo.byId(fieldId);
1001     var tip = dojo.byId(fieldId + '-tip');
1002     dojo.connect(fld, 'onfocus', function(evt) {
1003                      dijit.showTooltip(tip.innerHTML, fld, ['below', 'after']); });
1004     dojo.connect(fld, 'onblur', function(evt) { dijit.hideTooltip(fld); });
1005 }
1006
1007 // xpath utilities
1008
1009 var xpathParser = new openils.MarcXPathParser();
1010
1011 function tagAndSubFieldsToXpath(tags, subfields) {
1012     // given tags, and subfields, build up an XPath.
1013     try {
1014         var parts = {
1015             'tags':tags.match(/[\d]+/g), 
1016             'subfields':subfields.match(/[a-zA-z]/g) };
1017         return xpathParser.compile(parts);
1018     } catch (err) {
1019         return {'parts':null, 'tags':null, 'error':err};
1020     }
1021 }
1022
1023 function looksLikeDerivedXpath(path) {
1024     // Does this path look like it was derived from tags and subfields?
1025     var parsed = xpathParser.parse(path);
1026     if (parsed.tags == null) 
1027         return false;
1028     var compiled = xpathParser.compile(parsed);
1029     return (path == compiled);
1030 }
1031
1032 // amazing xpath-util unit-tests
1033 if (!looksLikeDerivedXpath('//*[@tag="901"]/*[@code="c"]'))     alert('vandelay xpath-utility error');
1034 if ( looksLikeDerivedXpath('ba-boo-ba-boo!'))                   alert('vandelay xpath-utility error');