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