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