9c9ac8526db5411e95e9578df5e661656edda866
[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.Dialog");
24 dojo.require("dojo.cookie");
25 dojo.require("dojox.grid.Grid");
26 dojo.require("dojo.data.ItemFileReadStore");
27 dojo.require('dojo.date.locale');
28 dojo.require('dojo.date.stamp');
29 dojo.require("fieldmapper.Fieldmapper");
30 dojo.require("fieldmapper.dojoData");
31 dojo.require('openils.CGI');
32 dojo.require('openils.User');
33 dojo.require('openils.Event');
34 dojo.require('openils.MarcXPathParser');
35 dojo.require('openils.GridColumnPicker');
36
37 var globalDivs = [
38     'vl-generic-progress',
39     'vl-generic-progress-with-total',
40     'vl-marc-upload-div',
41     'vl-queue-div',
42     'vl-match-div',
43     'vl-match-html-div',
44     'vl-queue-select-div',
45     'vl-marc-upload-status-div'
46 ];
47
48 var authtoken;
49 var VANDELAY_URL = '/vandelay-upload';
50 var bibAttrDefs = [];
51 var authAttrDefs = [];
52 var queuedRecords = [];
53 var queuedRecordsMap = {};
54 var bibAttrsFetched = false;
55 var authAttrsFetched = false;
56 var attrDefMap = {}; // maps attr def code names to attr def ids
57 var currentType;
58 var currentQueueId = null;
59 var userCache = {};
60 var currentMatchedRecords; // set of loaded matched bib records
61 var currentOverlayRecordsMap; // map of import record to overlay record
62 var currentImportRecId; // when analyzing matches, this is the current import record
63 var userBibQueues;
64 var userAuthQueues;
65 var selectableGridRecords;
66 var cgi = new openils.CGI();
67 var vlQueueGridColumePicker;
68
69 /**
70   * Grab initial data
71   */
72 function vlInit() {
73     authtoken = dojo.cookie('ses') || cgi.param('ses');
74     var initNeeded = 4; // how many async responses do we need before we're init'd 
75     var initCount = 0; // how many async reponses we've received
76
77     function checkInitDone() {
78         initCount++;
79         if(initCount == initNeeded)
80             runStartupCommands();
81     }
82
83     // Fetch the bib and authority attribute definitions
84     fieldmapper.standardRequest(
85         ['open-ils.permacrud', 'open-ils.permacrud.search.vqbrad'],
86         {   async: true,
87             params: [authtoken, {id:{'!=':null}}],
88             onresponse: function(r) {
89                 var def = r.recv().content(); 
90                 if(e = openils.Event.parse(def[0])) 
91                     return alert(e);
92                 bibAttrDefs.push(def);
93             },
94             oncomplete: function() {
95                 bibAttrDefs = bibAttrDefs.sort(
96                     function(a, b) {
97                         if(a.id() > b.id()) return 1;
98                         if(a.id() < b.id()) return -1;
99                         return 0;
100                     }
101                 );
102                 checkInitDone();
103             }
104         }
105     );
106
107     fieldmapper.standardRequest(
108         ['open-ils.permacrud', 'open-ils.permacrud.search.vqarad'],
109         {   async: true,
110             params: [authtoken, {id:{'!=':null}}],
111             onresponse: function(r) {
112                 var def = r.recv().content(); 
113                 if(e = openils.Event.parse(def[0])) 
114                     return alert(e);
115                 authAttrDefs.push(def);
116             },
117             oncomplete: function() {
118                 authAttrDefs = authAttrDefs.sort(
119                     function(a, b) {
120                         if(a.id() > b.id()) return 1;
121                         if(a.id() < b.id()) return -1;
122                         return 0;
123                     }
124                 );
125                 checkInitDone();
126             }
127         }
128     );
129
130     fieldmapper.standardRequest(
131         ['open-ils.vandelay', 'open-ils.vandelay.bib_queue.owner.retrieve.atomic'],
132         {   async: true,
133             params: [authtoken],
134             oncomplete: function(r) {
135                 var list = r.recv().content();
136                 if(e = openils.Event.parse(list[0]))
137                     return alert(e);
138                 userBibQueues = list;
139                 checkInitDone();
140             }
141         }
142     );
143
144     fieldmapper.standardRequest(
145         ['open-ils.vandelay', 'open-ils.vandelay.authority_queue.owner.retrieve.atomic'],
146         {   async: true,
147             params: [authtoken],
148             oncomplete: function(r) {
149                 var list = r.recv().content();
150                 if(e = openils.Event.parse(list[0]))
151                     return alert(e);
152                 userAuthQueues = list;
153                 checkInitDone();
154             }
155         }
156     );
157 }
158
159 function displayGlobalDiv(id) {
160     for(var i = 0; i < globalDivs.length; i++) {
161         try {
162             dojo.style(dojo.byId(globalDivs[i]), 'display', 'none');
163         } catch(e) {
164             alert('please define div ' + globalDivs[i]);
165         }
166     }
167     dojo.style(dojo.byId(id),'display','block');
168 }
169
170 function runStartupCommands() {
171     currentQueueId = cgi.param('qid');
172     currentType = cgi.param('qtype');
173     if(currentQueueId)
174         return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
175     vlShowUploadForm();
176 }
177
178 /**
179   * asynchronously upload a file of MARC records
180   */
181 function uploadMARC(onload){
182     dojo.byId('vl-upload-status-count').innerHTML = '0';
183     dojo.byId('vl-ses-input').value = authtoken;
184     displayGlobalDiv('vl-marc-upload-status-div');
185     dojo.io.iframe.send({
186         url: VANDELAY_URL,
187         method: "post",
188         handleAs: "html",
189         form: dojo.byId('vl-marc-upload-form'),
190         handle: function(data,ioArgs){
191             var content = data.documentElement.textContent;
192             onload(content);
193         }
194     });
195 }       
196
197 /**
198   * Creates a new vandelay queue
199   */
200 function createQueue(queueName, type, onload) {
201     fieldmapper.standardRequest(
202         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.create'],
203         {   async: true,
204             params: [authtoken, queueName, null, type],
205             oncomplete : function(r) {
206                 var queue = r.recv().content();
207                 if(e = openils.Event.parse(queue)) 
208                     return alert(e);
209                 onload(queue);
210             }
211         }
212     );
213 }
214
215 /**
216   * Tells vendelay to pull a batch of records from the cache and explode them
217   * out into the vandelay tables
218   */
219 function processSpool(key, queueId, type, onload) {
220     fieldmapper.standardRequest(
221         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'.process_spool'],
222         {   async: true,
223             params: [authtoken, key, queueId],
224             onresponse : function(r) {
225                 var resp = r.recv().content();
226                 if(e = openils.Event.parse(resp)) 
227                     return alert(e);
228                 dojo.byId('vl-upload-status-count').innerHTML = resp;
229             },
230             oncomplete : function(r) {onload();}
231         }
232     );
233 }
234
235 function retrieveQueuedRecords(type, queueId, onload) {
236     displayGlobalDiv('vl-generic-progress');
237     queuedRecords = [];
238     queuedRecordsMap = {};
239     currentOverlayRecordsMap = {};
240     selectableGridRecords = {};
241     resetVlQueueGridLayout();
242
243     var method = 'open-ils.vandelay.'+type+'_queue.records.retrieve.atomic';
244     if(vlQueueGridShowMatches.checked)
245         method = method.replace('records', 'records.matches');
246
247     var limit = parseInt(vlQueueDisplayLimit.getValue());
248     var offset = limit * parseInt(vlQueueDisplayPage.getValue()-1);
249
250     fieldmapper.standardRequest(
251         ['open-ils.vandelay', method],
252         {   async: true,
253             params: [authtoken, queueId, 
254                 {   clear_marc: 1, 
255                     offset: offset,
256                     limit: limit
257                 }
258             ],
259             /* intermittent bug in streaming, multipart requests prevents use of onreponse for now...
260             onresponse: function(r) {
261                 var rec = r.recv().content();
262                 if(e = openils.Event.parse(rec))
263                     return alert(e);
264                 queuedRecords.push(rec);
265                 queuedRecordsMap[rec.id()] = rec;
266             },
267             */
268             oncomplete: function(r){
269                 var recs = r.recv().content();
270                 if(e = openils.Event.parse(recs[0]))
271                     return alert(e);
272                 for(var i = 0; i < recs.length; i++) {
273                     var rec = recs[i];
274                     queuedRecords.push(rec);
275                     queuedRecordsMap[rec.id()] = rec;
276                 }
277                 onload();
278             }
279         }
280     );
281 }
282
283 function vlLoadMatchUI(recId, attrCode) {
284     displayGlobalDiv('vl-generic-progress');
285     var matches = getRecMatchesFromAttrCode(queuedRecordsMap[recId], attrCode);
286     var records = [];
287     currentImportRecId = recId;
288     for(var i = 0; i < matches.length; i++)
289         records.push(matches[i].eg_record());
290
291     var retrieve = ['open-ils.search', 'open-ils.search.biblio.record_entry.slim.retrieve'];
292     var params = [records];
293     if(currentType == 'auth') {
294         retrieve = ['open-ils.cat', 'open-ils.cat.authority.record.retrieve'];
295         parmas = [authtoken, records, {clear_marc:1}];
296     }
297
298     fieldmapper.standardRequest(
299         retrieve,
300         {   async: true,
301             params:params,
302             oncomplete: function(r) {
303                 var recs = r.recv().content();
304                 if(e = openils.Event.parse(recs))
305                     return alert(e);
306
307                 /* ui mangling */
308                 displayGlobalDiv('vl-match-div');
309                 resetVlMatchGridLayout();
310                 currentMatchedRecords = recs;
311                 vlMatchGrid.setStructure(vlMatchGridLayout);
312
313                 // build the data store or records with match information
314                 var dataStore = bre.toStoreData(recs, null, {virtualFields:['field_type']});
315                 for(var i = 0; i < dataStore.items.length; i++) {
316                     var item = dataStore.items[i];
317                     for(var j = 0; j < matches.length; j++) {
318                         var match = matches[j];
319                         if(match.eg_record() == item.id)
320                             item.field_type = match.field_type();
321                     }
322                 }
323                 // now populate the grid
324                 vlPopulateGrid(vlMatchGrid, dataStore);
325             }
326         }
327     );
328 }
329
330 function vlPopulateGrid(grid, data) {
331     var store = new dojo.data.ItemFileReadStore({data:data});
332     var model = new dojox.grid.data.DojoData(
333         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
334     grid.setModel(model);
335     grid.update();
336 }
337
338
339 function vlLoadMARCHtml(recId) {
340     displayGlobalDiv('vl-generic-progress');
341     var api = ['open-ils.search', 'open-ils.search.biblio.record.html'];
342     if(currentType == 'auth')
343         api = ['open-ils.search', 'open-ils.search.authority.to_html'];
344     fieldmapper.standardRequest(
345         api, 
346         {   async: true,
347             params: [recId, 1],
348             oncomplete: function(r) {
349             displayGlobalDiv('vl-match-html-div');
350                 var html = r.recv().content();
351                 dojo.byId('vl-match-record-html').innerHTML = html;
352             }
353         }
354     );
355 }
356
357
358 /**
359   * Given a record, an attribute definition code, and a matching record attribute,
360   * this will determine if there are any import matches and build the UI to
361   * represent those matches.  If no matches exist, simply returns the attribute value
362   */
363 function buildAttrColumnUI(rec, attrCode, attr) {
364     var matches = getRecMatchesFromAttrCode(rec, attrCode);
365     if(matches.length > 0) { // found some matches
366         return '<div class="match_div">' +
367             '<a href="javascript:void(0);" onclick="vlLoadMatchUI('+
368             rec.id()+',\''+attrCode+'\');">'+ 
369             attr.attr_value() + '&nbsp;('+matches.length+')</a></div>';
370     }
371
372     return attr.attr_value();
373 }
374
375 function getRecMatchesFromAttrCode(rec, attrCode) {
376     var matches = [];
377     var attr = getRecAttrFromCode(rec, attrCode);
378     for(var j = 0; j < rec.matches().length; j++) {
379         var match = rec.matches()[j];
380         if(match.matched_attr() == attr.id()) 
381             matches.push(match);
382     }
383     return matches;
384 }
385
386 function getRecAttrFromCode(rec, attrCode) {
387     var defId = attrDefMap[attrCode];
388     var attrs = rec.attributes();
389     for(var i = 0; i < attrs.length; i++) {
390         var attr = attrs[i];
391         if(attr.field() == defId) 
392             return attr;
393     }
394     return null;
395 }
396
397 function getAttrValue(rowIdx) {
398     var data = this.grid.model.getRow(rowIdx);
399     if(!data) return '';
400     var attrCode = this.field.split('.')[1];
401     var rec = queuedRecordsMap[data.id];
402     var attr = getRecAttrFromCode(rec, attrCode);
403     if(attr)
404         return buildAttrColumnUI(rec, attrCode, attr);
405     return '';
406 }
407
408 function vlGetDateTimeField(rowIdx) {
409     data = this.grid.model.getRow(rowIdx);
410     if(!data) return '';
411     if(!data[this.field]) return '';
412     var date = dojo.date.stamp.fromISOString(data[this.field]);
413     return dojo.date.locale.format(date, {selector:'date'});
414 }
415
416 function vlGetCreator(rowIdx) {
417     data = this.grid.model.getRow(rowIdx);
418     if(!data) return '';
419     var id = data.creator;
420     if(userCache[id])
421         return userCache[id].usrname();
422     var user = fieldmapper.standardRequest(
423         ['open-ils.actor', 'open-ils.actor.user.retrieve'], [authtoken, id]);
424     if(e = openils.Event.parse(user))
425         return alert(e);
426     userCache[id] = user;
427     return user.usrname();
428 }
429
430 function vlGetViewMARC(rowIdx) {
431     data = this.grid.model.getRow(rowIdx);
432     if(data) 
433         return this.value.replace('RECID', data.id);
434 }
435
436 function vlGetOverlayTargetSelector(rowIdx) {
437     data = this.grid.model.getRow(rowIdx);
438     if(data) {
439         var value = this.value.replace('ID', data.id);
440         var overlay = currentOverlayRecordsMap[currentImportRecId];
441         if(overlay && overlay == data.id) 
442             value = value.replace('/>', 'checked="checked"/>');
443         return value;
444     }
445 }
446
447 /**
448   * see if the user has enabled overlays for the current match set and, 
449   * if so, map the current import record to the overlay target.
450   */
451 function vlHandleOverlayTargetSelected() {
452     if(vlOverlayTargetEnable.checked) {
453         for(var i = 0; i < currentMatchedRecords.length; i++) {
454             var matchRecId = currentMatchedRecords[i].id();
455             if(dojo.byId('vl-overlay-target-'+matchRecId).checked) {
456                 console.log("found overlay target " + matchRecId);
457                 currentOverlayRecordsMap[currentImportRecId] = matchRecId;
458                 dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = true;
459                 dojo.byId('vl-record-list-selected-' + currentImportRecId).parentNode.className = 'overlay_selected';
460                 return;
461             }
462         }
463     } else {
464         delete currentOverlayRecordsMap[currentImportRecId];
465         dojo.byId('vl-record-list-selected-' + currentImportRecId).checked = false;
466     }
467 }
468
469 var vlQueueGridBuilt = false;
470 function buildRecordGrid(type) {
471     displayGlobalDiv('vl-queue-div');
472
473     currentOverlayRecordsMap = {};
474
475     if(!vlQueueGridBuilt) {
476         var defs = (type == 'bib') ? bibAttrDefs : authAttrDefs;
477         for(var i = 0; i < defs.length; i++) {
478             var def = defs[i]
479             attrDefMap[def.code()] = def.id();
480             var col = {
481                 name:def.description(), 
482                 field:'attr.' + def.code(),
483                 get: getAttrValue,
484                 selectableColumn:true
485             };
486             vlQueueGridLayout[0].cells[0].push(col);
487         }
488         vlQueueGridBuilt = true;
489     }
490
491     var storeData;
492     if(type == 'bib')
493         storeData = vqbr.toStoreData(queuedRecords);
494     else
495         storeData = vqar.toStoreData(queuedRecords);
496
497     var store = new dojo.data.ItemFileReadStore({data:storeData});
498     var model = new dojox.grid.data.DojoData(
499         null, store, {rowsPerPage: 100, clientSort: true, query:{id:'*'}});
500
501     vlQueueGrid.setModel(model);
502     if(vlQueueGridColumePicker) 
503         vlQueueGrid.setStructure(vlQueueGridColumePicker.structure);
504     else
505         vlQueueGrid.setStructure(vlQueueGridLayout);
506     vlQueueGrid.update();
507
508     if(!vlQueueGridColumePicker) {
509         vlQueueGridColumePicker = 
510             new openils.GridColumnPicker(vlQueueGridColumePickerDialog, vlQueueGrid);
511     }
512 }
513
514 function vlDeleteQueue(type, queueId, onload) {
515     fieldmapper.standardRequest(
516         ['open-ils.vandelay', 'open-ils.vandelay.'+type+'_queue.delete'],
517         {   async: true,
518             params: [authtoken, queueId],
519             oncomplete: function(r) {
520                 var resp = r.recv().content();
521                 if(e = openils.Event.parse(resp))
522                     return alert(e);
523                 onload();
524             }
525         }
526     );
527 }
528
529
530 function vlQueueGridDrawSelectBox(rowIdx) {
531     var data = this.grid.model.getRow(rowIdx);
532     if(!data) return '';
533     var domId = 'vl-record-list-selected-' +data.id;
534     selectableGridRecords[domId] = data.id;
535     return "<div><input type='checkbox' id='"+domId+"'/></div>";
536 }
537
538 function vlSelectAllQueueGridRecords() {
539     for(var id in selectableGridRecords) 
540         dojo.byId(id).checked = true;
541 }
542 function vlSelectNoQueueGridRecords() {
543     for(var id in selectableGridRecords) 
544         dojo.byId(id).checked = false;
545 }
546 function vlToggleQueueGridSelect() {
547     if(dojo.byId('vl-queue-grid-row-selector').checked)
548         vlSelectAllQueueGridRecords();
549     else
550         vlSelectNoQueueGridRecords();
551 }
552
553 var handleRetrieveRecords = function() {
554     buildRecordGrid(currentType);
555 }
556
557 function vlImportSelectedRecords() {
558     displayGlobalDiv('vl-generic-progress-with-total');
559     var records = [];
560
561     for(var id in selectableGridRecords) {
562         if(dojo.byId(id).checked) {
563             var recId = selectableGridRecords[id];
564             var rec = queuedRecordsMap[recId];
565             if(!rec.import_time()) 
566                 records.push(recId);
567         }
568     }
569
570     fieldmapper.standardRequest(
571         ['open-ils.vandelay', 'open-ils.vandelay.'+currentType+'_record.list.import'],
572         {   async: true,
573             params: [authtoken, records, {overlay_map:currentOverlayRecordsMap}],
574             onresponse: function(r) {
575                 var resp = r.recv().content();
576                 if(e = openils.Event.parse(resp))
577                     return alert(e);
578                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
579             },
580             oncomplete: function() {
581                 return retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
582             }
583         }
584     );
585 }
586
587 function vlImportRecordQueue(type, queueId, noMatchOnly, onload) {
588     displayGlobalDiv('vl-generic-progress-with-total');
589     var method = 'open-ils.vandelay.bib_queue.import';
590     if(noMatchOnly)
591         method = method.replace('import', 'nomatch.import');
592     if(type == 'auth')
593         method = method.replace('bib', 'auth');
594
595     fieldmapper.standardRequest(
596         ['open-ils.vandelay', method],
597         {   async: true,
598             params: [authtoken, queueId],
599             onresponse: function(r) {
600                 var resp = r.recv().content();
601                 if(e = openils.Event.parse(resp))
602                     return alert(e);
603                 vlControlledProgressBar.update({maximum:resp.total, progress:resp.progress});
604             },
605             oncomplete: function() {onload();}
606         }
607     );
608 }
609
610
611 /**
612   * Create queue, upload MARC, process spool, load the newly created queue 
613   */
614 function batchUpload() {
615     var queueName = dijit.byId('vl-queue-name').getValue();
616     currentType = dijit.byId('vl-record-type').getValue();
617
618     var handleProcessSpool = function() {
619         console.log('records uploaded and spooled');
620         if(vlUploadQueueAutoImport.checked) {
621             vlImportRecordQueue(currentType, currentQueueId, true,  
622                 function() {
623                     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
624                 }
625             );
626         } else {
627             retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
628         }
629     }
630
631     var handleUploadMARC = function(key) {
632         console.log('marc uploaded');
633         dojo.style(dojo.byId('vl-upload-status-processing'), 'display', 'block');
634         processSpool(key, currentQueueId, currentType, handleProcessSpool);
635     };
636
637     var handleCreateQueue = function(queue) {
638         console.log('queue created ' + queue.name());
639         currentQueueId = queue.id();
640         uploadMARC(handleUploadMARC);
641     };
642     
643     if(vlUploadQueueSelector.getValue() && !queueName) {
644         currentQueueId = vlUploadQueueSelector.getValue();
645         console.log('adding records to existing queue ' + currentQueueId);
646         uploadMARC(handleUploadMARC);
647     } else {
648         createQueue(queueName, currentType, handleCreateQueue);
649     }
650 }
651
652
653 function vlFleshQueueSelect(selector, type) {
654     var data = (type == 'bib') ? vbq.toStoreData(userBibQueues) : vaq.toStoreData(userAuthQueues);
655     selector.store = new dojo.data.ItemFileReadStore({data:data});
656     selector.setValue(null);
657     selector.setDisplayedValue('');
658     if(data[0])
659         selector.setValue(data[0].id());
660 }
661
662 function vlShowUploadForm() {
663     displayGlobalDiv('vl-marc-upload-div');
664     vlFleshQueueSelect(vlUploadQueueSelector, vlUploadRecordType.getValue());
665 }
666
667 function vlShowQueueSelect() {
668     displayGlobalDiv('vl-queue-select-div');
669     vlFleshQueueSelect(vlQueueSelectQueueList, vlQueueSelectType.getValue());
670 }
671
672 function vlFetchQueueFromForm() {
673     currentType = vlQueueSelectType.getValue();
674     currentQueueId = vlQueueSelectQueueList.getValue();
675     retrieveQueuedRecords(currentType, currentQueueId, handleRetrieveRecords);
676 }
677
678 dojo.addOnLoad(vlInit);