This commit was manufactured by cvs2svn to create branch 'rel_1_0'.
author(no author) <(no author)@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Mon, 14 May 2007 02:46:12 +0000 (02:46 +0000)
committer(no author) <(no author)@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Mon, 14 May 2007 02:46:12 +0000 (02:46 +0000)
git-svn-id: svn://svn.open-ils.org/ILS/branches/rel_1_0@7302 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/src/sql/Pg/example.reporter-extension.sql [new file with mode: 0644]
Open-ILS/web/reports/xul/operators.js [new file with mode: 0644]
Open-ILS/web/reports/xul/reporter.css [new file with mode: 0644]
Open-ILS/web/reports/xul/source-browse.js [new file with mode: 0644]
Open-ILS/web/reports/xul/source-setup.js [new file with mode: 0644]
Open-ILS/web/reports/xul/template-config.js [new file with mode: 0644]
Open-ILS/web/reports/xul/template_builder.xul [new file with mode: 0644]
Open-ILS/web/reports/xul/transforms.js [new file with mode: 0644]
Open-ILS/web/reports/xul/utilities.js [new file with mode: 0644]
Open-ILS/web/reports/xul/xulbuilder.js [new file with mode: 0644]

diff --git a/Open-ILS/src/sql/Pg/example.reporter-extension.sql b/Open-ILS/src/sql/Pg/example.reporter-extension.sql
new file mode 100644 (file)
index 0000000..6d1e375
--- /dev/null
@@ -0,0 +1,122 @@
+BEGIN;
+
+CREATE OR REPLACE VIEW reporter.classic_current_circ AS
+SELECT cl.shortname AS circ_lib,
+       cl.id AS circ_lib_id,
+       circ.xact_start AS xact_start,
+       circ_type.type AS circ_type,
+       cp.id AS copy_id,
+       cp.circ_modifier,
+       ol.shortname AS owning_lib_name,
+       lm.value AS language,
+       lfm.value AS lit_form,
+       ifm.value AS item_form,
+       itm.value AS item_type,
+       sl.name AS shelving_location,
+       p.id AS patron_id,
+       g.name AS profile_group,
+       dem.general_division AS demographic_general_division,
+       circ.id AS id,
+       cn.id AS call_number,
+       cn.label AS call_number_label,
+       call_number_dewey(cn.label) AS dewey,
+       CASE
+               WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
+                       THEN
+                               btrim(
+                                       to_char(
+                                               10 * floor((call_number_dewey(cn.label)::float) / 10), '000'
+                                       )
+                               )
+               ELSE NULL
+       END AS dewey_block_tens,
+       CASE
+               WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
+                       THEN
+                               btrim(
+                                       to_char(
+                                               100 * floor((call_number_dewey(cn.label)::float) / 100), '000'
+                                       )
+                               )
+               ELSE NULL
+       END AS dewey_block_hundreds,
+       CASE
+               WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
+                       THEN
+                               btrim(
+                                       to_char(
+                                               10 * floor((call_number_dewey(cn.label)::float) / 10), '000'
+                                       )
+                               )
+                               || '-' ||
+                               btrim(
+                                       to_char(
+                                               10 * floor((call_number_dewey(cn.label)::float) / 10) + 9, '000'
+                                       )
+                               )
+               ELSE NULL
+       END AS dewey_range_tens,
+       CASE
+               WHEN call_number_dewey(cn.label) ~  E'^[0-9.]+$'
+                       THEN
+                               btrim(
+                                       to_char(
+                                               100 * floor((call_number_dewey(cn.label)::float) / 100), '000'
+                                       )
+                               )
+                               || '-' ||
+                               btrim(
+                                       to_char(
+                                               100 * floor((call_number_dewey(cn.label)::float) / 100) + 99, '000'
+                                       )
+                               )
+               ELSE NULL
+       END AS dewey_range_hundreds,
+       hl.id AS patron_home_lib,
+       hl.shortname AS patron_home_lib_shortname,
+       paddr.county AS patron_county,
+       paddr.city AS patron_city,
+       paddr.post_code AS patron_zip,
+       sc1.stat_cat_entry AS stat_cat_1,
+       sc2.stat_cat_entry AS stat_cat_2,
+       sce1.value AS stat_cat_1_value,
+       sce2.value AS stat_cat_2_value
+  FROM action.circulation circ
+       JOIN reporter.circ_type circ_type ON (circ.id = circ_type.id)
+       JOIN asset.copy cp ON (cp.id = circ.target_copy)
+       JOIN asset.copy_location sl ON (cp.location = sl.id)
+       JOIN asset.call_number cn ON (cp.call_number = cn.id)
+       JOIN actor.org_unit ol ON (cn.owning_lib = ol.id)
+       JOIN metabib.rec_descriptor rd ON (rd.record = cn.record)
+       JOIN actor.org_unit cl ON (circ.circ_lib = cl.id)
+       JOIN actor.usr p ON (p.id = circ.usr)
+       JOIN actor.org_unit hl ON (p.home_ou = hl.id)
+       JOIN permission.grp_tree g ON (p.profile = g.id)
+       JOIN reporter.demographic dem ON (dem.id = p.id)
+       JOIN actor.usr_address paddr ON (paddr.id = p.billing_address)
+       LEFT JOIN config.language_map lm ON (rd.item_lang = lm.code)
+       LEFT JOIN config.lit_form_map lfm ON (rd.lit_form = lfm.code)
+       LEFT JOIN config.item_form_map ifm ON (rd.item_form = ifm.code)
+       LEFT JOIN config.item_type_map itm ON (rd.item_type = itm.code)
+       LEFT JOIN asset.stat_cat_entry_copy_map sc1 ON (sc1.owning_copy = cp.id AND sc1.stat_cat = 1)
+       LEFT JOIN asset.stat_cat_entry sce1 ON (sce1.id = sc1.stat_cat_entry)
+       LEFT JOIN asset.stat_cat_entry_copy_map sc2 ON (sc2.owning_copy = cp.id AND sc2.stat_cat = 2)
+       LEFT JOIN asset.stat_cat_entry sce2 ON (sce2.id = sc2.stat_cat_entry);
+
+CREATE OR REPLACE VIEW reporter.legacy_cat1 AS
+SELECT id,
+       owner,
+       value
+  FROM asset.stat_cat_entry
+  WHERE        stat_cat = 1;
+
+CREATE OR REPLACE VIEW reporter.legacy_cat2 AS
+SELECT id,
+       owner,
+       value
+  FROM asset.stat_cat_entry
+  WHERE        stat_cat = 2;
+
+
+COMMIT;
+
diff --git a/Open-ILS/web/reports/xul/operators.js b/Open-ILS/web/reports/xul/operators.js
new file mode 100644 (file)
index 0000000..81982fd
--- /dev/null
@@ -0,0 +1,60 @@
+
+var OILS_RPT_FILTERS = {
+       '=' : {
+               label : 'Equals',
+       },
+
+       'like' : {
+               label : 'Contains Matching substring',
+       }, 
+
+       ilike : {
+               label : 'Contains Matching substring (ignore case)',
+       },
+
+       '>' : {
+               label : 'Greater than',
+               labels : { timestamp : 'After (Date/Time)' }
+       },
+
+       '>=' : {
+               label : 'Greater than or equal to',
+               labels : { timestamp : 'On or After (Date/Time)' }
+       }, 
+
+
+       '<' : {
+               label : 'Less than',
+               labels : { timestamp : 'Before (Date/Time)' }
+       }, 
+
+       '<=' : {
+               label : 'Less than or equal to', 
+               labels : { timestamp : 'On or Before (Date/Time)' }
+       },
+
+       'in' : {
+               label : 'In list',
+       },
+
+       'not in' : {
+               label : 'Not in list',
+       },
+
+       'between' : {
+               label : 'Between',
+       },
+
+       'not between' : {
+               label : 'Not between',
+       },
+
+       'is' : {
+               label : 'Is NULL'
+       },
+
+       'is not' : {
+               label : 'Is not NULL'
+       }
+}
+
diff --git a/Open-ILS/web/reports/xul/reporter.css b/Open-ILS/web/reports/xul/reporter.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Open-ILS/web/reports/xul/source-browse.js b/Open-ILS/web/reports/xul/source-browse.js
new file mode 100644 (file)
index 0000000..5bc9302
--- /dev/null
@@ -0,0 +1,190 @@
+
+function sourceTreeHandlerDblClick (ev) { return sourceTreeHandler(ev,true) }
+
+function sourceTreeHandler (ev, dbl) {
+
+       var row = {}, col = {}, part = {}, item;
+       var tree = $('idl-browse-tree');
+
+
+       try {
+               tree.treeBoxObject.getCellAt(ev.clientX, ev.clientY, row, col, part);
+               item = tree.contentView.getItemAtIndex(row.value);
+       } catch (e) {
+               // ... meh
+       }
+
+       if (part.value == 'twisty' || dbl) { // opening or closing
+               if (item.getAttribute('open') == 'true' && item.lastChild.childNodes.length == 0) {
+                       //var subtree = item.lastChild;
+                       //while (subtree.childNodes.length)
+                       //      subtree.removeChild(subtree.lastChild);
+
+                       var p = getIDLClass( item.getAttribute('idlclass') );
+
+                       var subtreeList = [];
+                       var link_fields = p.getElementsByTagName('link');
+
+                       for ( var i = 0; i < link_fields.length; i++ ) {
+                               var field = getIDLField( p, link_fields[i].getAttribute('field') );
+
+                               if (!field) continue;
+
+                               var name = field.getAttributeNS(rptNS,'label');
+                               if (!name) name = field.getAttribute('name');
+
+                               var idlclass = link_fields[i].getAttribute('class');
+                               var map = link_fields[i].getAttribute('map');
+                               var link = link_fields[i].getAttribute('field');
+                               var key = link_fields[i].getAttribute('key');
+                               var reltype = link_fields[i].getAttribute('reltype');
+
+                               if (map) continue;
+
+                               var pathList = [];
+                               findAnscestorStack( item, 'treeitem', pathList );
+
+                               var fullpath = '';
+                               for (var j in pathList.reverse()) {
+                                       var n = pathList[j].getAttribute('idlclass');
+                                       var f = pathList[j].getAttribute('field');
+
+                                       if (f) fullpath += "-" + f;
+
+                                       if (fullpath) fullpath += ".";
+                                       fullpath += n;
+
+                               }
+
+                               fullpath += "-" + link;
+
+                               subtreeList.push(
+                                       { name : name,
+                                         idlclass : idlclass,
+                                         map : map,
+                                         key : key,
+                                         field : field.getAttribute('name'),
+                                         reltype : reltype,
+                                         link : link,
+                                         fullpath : fullpath
+                                       }
+                               );
+                       }
+
+                       populateSourcesSubtree( item.lastChild, subtreeList );
+               }
+       } else if (item) {
+               var classtree = $('class-treetop');
+
+               while (classtree.childNodes.length)
+                       classtree.removeChild(classtree.lastChild);
+
+               var c = getIDLClass( item.getAttribute('idlclass') );
+
+               populateDetailTree(
+                       classtree,
+                       c,
+                       item
+               );
+       }
+
+       return true;
+}
+
+function transformSelectHandler (noswap) {
+       var transform_tree = $('trans-view');
+       var transform = getSelectedItems(transform_tree)[0];
+
+       if (transform) {
+               if (transform.getAttribute('aggregate') == 'true') {
+                       $( 'filter_tab' ).setAttribute('disabled','true');
+                       $( 'aggfilter_tab' ).setAttribute('disabled','false');
+
+                       if (!noswap && $( 'filter_tab' ).selected)
+                               $( 'filter_tab' ).parentNode.selectedItem = $( 'aggfilter_tab' );
+               } else {
+                       $( 'filter_tab' ).setAttribute('disabled','false');
+                       $( 'aggfilter_tab' ).setAttribute('disabled','true');
+
+                       if (!noswap && $( 'aggfilter_tab' ).selected)
+                               $( 'aggfilter_tab' ).parentNode.selectedItem = $( 'filter_tab' );
+               }
+       }
+
+       if ($( 'filter_tab' ).selected) {
+               if ($( 'filter_tab' ).getAttribute('disabled') == 'true')
+                       $( 'source-add' ).setAttribute('disabled','true');
+               else
+                       $( 'source-add' ).setAttribute('disabled','false');
+
+       } else if ($( 'aggfilter_tab' ).selected) {
+               if ($( 'aggfilter_tab' ).getAttribute('disabled') == 'true')
+                       $( 'source-add' ).setAttribute('disabled','true');
+               else
+                       $( 'source-add' ).setAttribute('disabled','false');
+
+       } else if ($( 'dis_tab' ).selected) {
+               $( 'source-add' ).setAttribute('disabled','false');
+       }
+}
+
+function detailTreeHandler (args) {
+       var class_tree = $('class-view');
+       var transform_tree = $('trans-treetop');
+
+       while (transform_tree.childNodes.length)
+               transform_tree.removeChild(transform_tree.lastChild);
+
+       var class_items = getSelectedItems(class_tree);;
+
+       var transforms = new Object(); 
+       for (var i in class_items) {
+               var item = class_items[i];
+               var dtype = item.lastChild.lastChild.getAttribute('label');
+
+               var item_transforms = getTransforms({ datatype : dtype });
+
+               for (var j in item_transforms) {
+                       transforms[item_transforms[j]] = OILS_RPT_TRANSFORMS[item_transforms[j]];
+                       transforms[item_transforms[j]].name = item_transforms[j];
+               }
+       }
+
+       var transformList = [];
+       for (var i in transforms) {
+               transformList.push( transforms[i] );
+       }
+
+       transformList.sort( sortHashLabels );
+
+       $( 'aggfilter_tab' ).setAttribute('disabled','true');
+
+       for (var i in transformList) {
+               var t = transformList[i];
+               transform_tree.appendChild(
+                       createTreeItem(
+                               { aggregate : t.aggregate,
+                                 name : t.name,
+                                 alias : t.label,
+                                 params : t.params,
+                               },
+                               createTreeRow(
+                                       {},
+                                       createTreeCell( { label : t.label } ),
+                                       createTreeCell( { label : t.params ? t.params : '0' } ),
+                                       createTreeCell( { label : t.datatype.length > 0 ?  t.datatype.join(', ') : 'all' } ),
+                                       createTreeCell( { label : t.aggregate ?  'Aggregate' : 'Non-Aggregate' } )
+                               )
+                       )
+               );
+
+               if (t.aggregate) $( 'aggfilter_tab' ).setAttribute('disabled','false');
+       }
+
+       transformSelectHandler(true);
+
+       return true;
+}
+
+
+
diff --git a/Open-ILS/web/reports/xul/source-setup.js b/Open-ILS/web/reports/xul/source-setup.js
new file mode 100644 (file)
index 0000000..4cf5222
--- /dev/null
@@ -0,0 +1,300 @@
+
+var idlNS      = "http://opensrf.org/spec/IDL/base/v1";
+var persistNS  = "http://open-ils.org/spec/opensrf/IDL/persistance/v1";
+var objNS      = "http://open-ils.org/spec/opensrf/IDL/objects/v1";
+var rptNS      = "http://open-ils.org/spec/opensrf/IDL/reporter/v1";
+var gwNS       = "http://opensrf.org/-/namespaces/gateway/v1";
+var xulNS      = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+var oilsIDL;
+var rpt_rel_cache = {};
+
+function sortHashLabels (a,b) { return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1; }
+
+function sortNames (a,b) {
+       var aname =  a.name.toLowerCase();
+       if (!aname) aname = a.idlclass.toLowerCase();
+
+       var bname =  b.name.toLowerCase();
+       if (!bname) bname = b.idlclass.toLowerCase();
+
+       return aname < bname ? -1 : 1;
+}
+
+function sortLabels (a,b) {
+       var aname =  a.getAttributeNS(rptNS, 'label').toLowerCase();
+       if (!aname) aname = a.getAttribute('name');
+       if (!aname) aname = a.getAttribute('id');
+
+       var bname =  b.getAttributeNS(rptNS, 'label').toLowerCase();
+       if (!bname) bname = b.getAttribute('name');
+       if (!bname) bname = b.getAttribute('id');
+
+       return aname < bname ? -1 : 1;
+}
+
+
+function loadTemplate(id) {
+       var cgi = new CGI();
+       var session = cgi.param('ses');
+
+       var r = new Request('open-ils.reporter:open-ils.reporter.template.retrieve', session, id);
+
+       r.callback(
+               function(res) {
+                       var tmpl = res.getResultObject();
+                       var template = JSON2js( tmpl.data() );
+
+                       resetUI( template.core_class );
+
+                       $('template-name').value = tmpl.name() + ' (clone)';
+                       $('template-description').value = tmpl.description();
+
+                       rpt_rel_cache = template.rel_cache;
+                       renderSources();
+               }
+       );
+
+       r.send();
+}
+
+
+function loadIDL() {
+        var req = new XMLHttpRequest();
+        req.open('GET', '../fm_IDL.xml', true);
+        req.onreadystatechange = function() {
+                if( req.readyState == 4 ) {
+                        oilsIDL = req.responseXML;
+                       populateSourcesMenu(
+                               filterByAttributeNS( oilsIDL.getElementsByTagName('class'), rptNS, 'core', 'true' )
+                       );
+
+                       var cgi = new CGI();
+                       var template_id = cgi.param('ct');
+                       if (template_id) loadTemplate(template_id);
+                }
+        }
+       req.send(null);
+
+}
+
+function getIDLClass (id) { return filterByAttribute( oilsIDL.getElementsByTagName('class'), 'id', id )[0] }
+function getIDLField (classNode,field) { return filterByAttribute( classNode.getElementsByTagName('field'), 'name', field )[0] }
+function getIDLLink (classNode,field) { return filterByAttribute( classNode.getElementsByTagName('link'), 'field', field )[0] }
+
+function resetUI (idlclass) {
+       if (getKeys(rpt_rel_cache).length > 0) {
+               if (!confirm(
+                       "You have started building a template!\n" +
+                       "Selecting a new starting source will destroy " +
+                       "the current template and start over.  Is this OK?"
+               )) return false;
+       }
+
+       rpt_rel_cache = {};
+       try { renderSources(); } catch (e) {}
+
+       populateSourcesTree( idlclass );
+
+       var tree = $('sources-treetop').parentNode;
+       tree.focus();
+       tree.view.selection.select(0);
+       tree.click();
+}
+
+function populateSourcesMenu (classList) {
+       classList.sort( sortLabels );
+
+       var menu = $('source-menu');
+
+       menu.appendChild(
+               createMenuItem(
+                       { label : 'Core Sources',
+                         disabled : 'true',
+                         style : 'color: black; text-decoration: underline;'
+                       }
+               )
+       );
+
+
+       for (var i in classList) {
+
+               var name = classList[i].getAttributeNS(rptNS,'label');
+               var id = classList[i].getAttribute('id');
+               if (!name) name = id;
+
+               menu.appendChild(
+                       createMenuItem(
+                               { container : 'true',
+                                 idlclass : id,
+                                 label : name,
+                                 onmouseup : 'resetUI( "' + id + '");'
+                               }
+                       )
+               );
+
+       }
+
+       menu.appendChild( createMenuSeparator() );
+
+       var _m = createMenu(
+               { label : 'All Available Sources' },
+               createMenuPopup(
+                       {},
+                       createMenuItem(
+                               { label : 'All Available Sources',
+                                 disabled : 'true',
+                                 style : 'color: black; '
+                               }
+                       ),
+                       createMenuSeparator()
+               )
+       );
+
+       menu.appendChild( _m );
+       menu = _m.firstChild;
+
+       var all = map(function(x){return x;}, oilsIDL.getElementsByTagNameNS(idlNS,'class'));
+       all.sort( sortLabels );
+
+       for (var i = 0; i < all.length; i++) {
+
+               if (all[i].getAttributeNS(persistNS,'virtual') == 'true') continue;
+
+               var name = all[i].getAttributeNS(rptNS,'label');
+               var id = all[i].getAttribute('id');
+               if (!name) name = id;
+
+               menu.appendChild(
+                       createMenuItem(
+                               { container : 'true',
+                                 idlclass : id,
+                                 label : name,
+                                 onmouseup : 'resetUI( "' + id + '");'
+                               }
+                       )
+               );
+
+       }
+
+
+}
+
+function populateSourcesTree (idlclass) {
+
+       var tcNode = $('sources-treetop');
+       while (tcNode.childNodes.length) tcNode.removeChild(tcNode.lastChild);
+
+       var c = getIDLClass( idlclass );
+       var name = c.getAttributeNS(rptNS,'label');
+       if (!name) name = idlclass;
+
+       tcNode.appendChild(
+               createTreeItem(
+                       { container : 'true',
+                         idlclass : idlclass,
+                         fullpath : idlclass
+                       },
+                       createTreeRow(
+                               { },
+                               createTreeCell( { label : name } )
+                       ),
+                       createTreeChildren( { alternatingbackground : true } )
+               )
+       );
+}
+
+function populateSourcesSubtree (tcNode, classList) {
+       classList.sort(sortNames);
+       for (var i in classList) {
+               var obj = classList[i];
+
+               var p = getIDLClass( obj.idlclass );
+               var cont = p.getElementsByTagName('link').length ? 'true' : 'false';
+
+               tcNode.appendChild(
+                       createTreeItem(
+                               { container : cont,
+                                 idlclass : obj.idlclass,
+                                 map : obj.map,
+                                 key : obj.key,
+                                 field : obj.field,
+                                 link : obj.link,
+                                 reltype : obj.reltype,
+                                 fullpath : obj.fullpath,
+                               },
+                               createTreeRow(
+                                       { },
+                                       createTreeCell( { label : obj.name } )
+                               ),
+                               createTreeChildren( { alternatingbackground : true } )
+                       )
+               );
+
+
+       }
+}
+
+function populateDetailTree (tcNode, c, item) {
+       var fullpath = item.getAttribute('fullpath');
+       var reltype = item.getAttribute('reltype');
+
+       var fields = filterByAttributeNS(c.getElementsByTagName('field'),persistNS, 'virtual','false');
+       fields.sort( sortLabels );
+
+       var id = c.getAttribute('id');
+       var path_label = [];
+
+       var steps = fullpath.split('.');
+       for (var k in steps) {
+
+               if (!steps[k]) continue;
+
+               var atom = steps[k].split('-');
+               var classNode = getIDLClass(atom[0]);
+
+               var _cname = classNode.getAttributeNS(rptNS, 'label');
+               if (!_cname) _cname = classNode.getAttribute('id');
+
+               var _label = _cname; 
+
+               if (atom.length > 1 && k == steps.length - 1) {
+                       var _f = getIDLField(classNode, atom[1]);
+                       var _fname = _f.getAttributeNS(rptNS, 'label');
+                       if (!_fname) _fname = _f.getAttribute('name');
+                       if (_fname) _label += ' :: ' + _fname; 
+               }
+
+               path_label.push(_label); 
+       }
+
+       $('path-label').value = path_label.join(' -> ');
+       $('path-label').setAttribute('reltype',reltype);
+
+       for (var i in fields) {
+
+               var type = fields[i].getAttributeNS(rptNS, 'datatype');
+               //if (!type) type = 'text';
+
+               var label = fields[i].getAttributeNS(rptNS, 'label');
+               var name = fields[i].getAttribute('name');
+               if (!label) label = name;
+
+               tcNode.appendChild(
+                       createTreeItem(
+                               { idlclass : id,
+                                 idlfield : name,
+                                 datatype : type,
+                                 fullpath : fullpath
+                               },
+                               createTreeRow(
+                                       { },
+                                       createTreeCell( { label : label } ),
+                                       createTreeCell( { label : type } )
+                               )
+                       )
+               );
+       }
+}
+
+
diff --git a/Open-ILS/web/reports/xul/template-config.js b/Open-ILS/web/reports/xul/template-config.js
new file mode 100644 (file)
index 0000000..c1d533d
--- /dev/null
@@ -0,0 +1,919 @@
+function removeReportAtom (args) {
+       if (!args) args = {};
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var fields = getSelectedItems(tree);
+
+
+       for (var i in fields) {
+               var field = fields[i];
+               var colname = field.firstChild.firstChild.nextSibling.getAttribute('label');
+               var relation_alias = field.getAttribute('relation');
+
+               delete rpt_rel_cache[relation_alias].fields[tabname][colname];
+               if (tabname == 'dis_tab') {
+                       var _o_tmp = [];
+                       for each (var _o_col in rpt_rel_cache.order_by) {
+                               if (_o_col.relation == relation_alias && _o_col.field == colname) continue;
+                               _o_tmp.push( _o_col );
+                       }
+                       rpt_rel_cache.order_by = _o_tmp
+               }
+
+               with (rpt_rel_cache[relation_alias].fields) {
+                       if ( getKeys(dis_tab).length == 0 && getKeys(filter_tab).length == 0 && getKeys(aggfilter_tab).length == 0 )
+                               delete rpt_rel_cache[relation_alias];
+               }
+       }
+
+       renderSources();
+
+       return true;
+}
+
+function addReportAtoms () {
+       var nope = $( 'source-add' ).getAttribute('disabled');
+       if (nope == 'true') return false;
+
+       var class_tree = $('class-view');
+       var transform_tree = $('trans-view');
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id')
+
+       var items = getSelectedItems(class_tree);
+       var transform = getSelectedItems(transform_tree)[0];
+
+       var reltype = $('path-label').getAttribute('reltype');
+       var class_label = $('path-label').value;
+       var relation_alias = hex_md5(class_label);
+
+       for (var i in items) {
+               var item = items[i];
+
+               var class_path = item.getAttribute('fullpath');
+               var field_class = item.getAttribute('idlclass');
+               var datatype = item.getAttribute('datatype');
+               var colname = item.getAttribute('idlfield');
+               var field_label = item.firstChild.firstChild.getAttribute('label');
+
+               var table_name = getIDLClass(field_class).getAttributeNS(persistNS,'tablename');
+
+               if ( !rpt_rel_cache[relation_alias] ) {
+                       rpt_rel_cache[relation_alias] =
+                               { label : class_label,
+                                 alias : relation_alias,
+                                 path  : class_path,
+                                 reltype  : reltype,
+                                 idlclass  : field_class,
+                                 table : table_name,
+                                 fields: { dis_tab : {}, filter_tab : {}, aggfilter_tab : {} }
+                               };
+               }
+
+               if ( !rpt_rel_cache[relation_alias].fields[tabname][colname] ) {
+                       rpt_rel_cache[relation_alias].fields[tabname][colname] =
+                               { colname   : colname,
+                                 transform : (transform && transform.getAttribute('name')) || 'Bare',
+                                 aggregate : transform && transform.getAttribute('aggregate'),
+                                 params    : transform && transform.getAttribute('params'),
+                                 transform_label: (transform && transform.getAttribute('alias')) || 'Raw Data',
+                                 alias     : field_label,
+                                 datatype  : datatype,
+                                 op        : '=',
+                                 op_label  : 'Equals',
+                                 op_value  : {},
+                               };
+
+                       if (!rpt_rel_cache.order_by)
+                               rpt_rel_cache.order_by = [];
+
+                       if (tabname == 'dis_tab')
+                               rpt_rel_cache.order_by.push( { relation : relation_alias, field : colname } );
+
+               } else if (
+                       confirm(
+                               'You have already added the [' + field_label +
+                               '] field\nfrom the [' + class_label + '] source. Click OK if you\nwould like ' +
+                               'to reset this field.'
+                       )
+               ) {
+                       rpt_rel_cache[relation_alias].fields[tabname][colname] =
+                               { colname   : colname,
+                                 transform : (transform && transform.getAttribute('name')) || 'Bare',
+                                 aggregate : transform && transform.getAttribute('aggregate'),
+                                 params    : transform && transform.getAttribute('params'),
+                                 transform_label: (transform && transform.getAttribute('alias')) || 'Raw Data',
+                                 alias     : field_label,
+                                 datatype  : datatype,
+                                 op        : '=',
+                                 op_label  : 'Equals',
+                                 op_value  : {},
+                               };
+               }
+       }
+
+       renderSources();
+
+       return true;
+}
+
+function changeDisplayOrder (dir) {
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var item = getSelectedItems(tree)[0];
+
+       var item_pos = tree.view.selection.currentIndex;
+
+       if (dir == 'u') {
+               if ( item.previousSibling ) {
+                       item.parentNode.insertBefore( item, item.previousSibling );
+                       item_pos--;
+               }
+       } else if (dir == 'd') {
+               if ( item.nextSibling ) {
+                       if (item.nextSibling.nextSibling ) item.parentNode.insertBefore( item, item.nextSibling.nextSibling );
+                       else item.parentNode.appendChild( item );
+                       item_pos++;
+               }
+       }
+       
+       rpt_rel_cache.order_by = [];
+       var ordered_list = tree.getElementsByTagName('treeitem');
+       for (var i = 0; i < ordered_list.length; i++) {
+               rpt_rel_cache.order_by.push(
+                       { relation : ordered_list[i].getAttribute('relation'),
+                         field    : ordered_list[i].firstChild.firstChild.nextSibling.getAttribute('label')
+                       }
+               );
+       }
+
+       tree.view.selection.select( item_pos );
+       return true;
+}
+
+function alterColumnLabel () {
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var item_pos = tree.view.selection.currentIndex;
+
+       var item = getSelectedItems(tree)[0];
+       var relation_alias = item.getAttribute('relation');
+
+       var field = item.firstChild.firstChild;
+       var colname = field.nextSibling.getAttribute('label');
+
+       var new_label = prompt(
+               "Change the column header to:",
+               field.getAttribute("label")
+       );
+
+       if (new_label) {
+               rpt_rel_cache[relation_alias].fields[tabname][colname].alias = new_label;
+               renderSources(true);
+               tree.view.selection.select( item_pos );
+               tree.focus();
+               tree.click();
+       }
+
+       return true;
+}
+
+function alterColumnTransform (trans) {
+
+       var transform = OILS_RPT_TRANSFORMS[trans];
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var item_pos = tree.view.selection.currentIndex;
+       var item =  getSelectedItems(tree)[0];
+       var relation_alias = item.getAttribute('relation');
+
+       var field = item.firstChild.firstChild;
+       var colname = field.nextSibling.getAttribute('label');
+
+       rpt_rel_cache[relation_alias].fields[tabname][colname].transform = trans;
+       rpt_rel_cache[relation_alias].fields[tabname][colname].aggregate = transform.aggregate;
+       rpt_rel_cache[relation_alias].fields[tabname][colname].params = transform.params;
+       rpt_rel_cache[relation_alias].fields[tabname][colname].transform_label = transform.label;
+
+       renderSources(true);
+       tree.view.selection.select( item_pos );
+       tree.focus();
+       tree.click();
+
+       $(tabname + '_trans_menu').hidePopup();
+       return true;
+}
+
+function changeOperator (args) {
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var item_pos = tree.view.selection.currentIndex;
+       var item = getSelectedItems(tree)[0];
+
+       var relation_alias = item.getAttribute('relation');
+
+       var field = item.firstChild.firstChild;
+       var colname = field.nextSibling.getAttribute('label');
+
+       rpt_rel_cache[relation_alias].fields[tabname][colname].op = args.op;
+       rpt_rel_cache[relation_alias].fields[tabname][colname].op_label = args.label;
+
+       renderSources(true);
+       tree.view.selection.select( item_pos );
+       tree.focus();
+       tree.click();
+
+       $(tabname + '_op_menu').hidePopup();
+       return true;
+}
+
+function removeTemplateFilterValue () {
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var item_pos = tree.view.selection.currentIndex;
+       var items = getSelectedItems(tree);
+
+       for (var i in items) {
+               var item = items[i];
+               var relation_alias = item.getAttribute('relation');
+
+               var field = item.firstChild.firstChild;
+               var colname = field.nextSibling.getAttribute('label');
+
+               rpt_rel_cache[relation_alias].fields[tabname][colname].op_value = {};
+       }
+
+       renderSources(true);
+       tree.view.selection.select( item_pos );
+       return true;
+}
+
+function timestampSetDate (obj, cal, date) {
+       obj.op_value.value = date;
+       obj.op_value.object = cal.date;
+       obj.op_value.label = '"' + date + '"';
+
+       renderSources(true);
+       return true;
+}
+
+var __handler_cache;
+
+function changeTemplateFilterValue () {
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var items = getSelectedItems(tree);
+
+       var targetCmd = $( tabname + '_value_action' );
+
+       targetCmd.menu = null;
+       targetCmd.command = null;
+       targetCmd.oncommand = null;
+       targetCmd.removeEventListener( 'command', __handler_cache, true );
+
+       for (var i in items) {
+               var item = items[i];
+               var relation_alias = item.getAttribute('relation');
+
+               var field = item.firstChild.firstChild;
+               var colname = field.nextSibling.getAttribute('label');
+
+               var obj = rpt_rel_cache[relation_alias].fields[tabname][colname]
+               var operation = OILS_RPT_FILTERS[obj.op];
+
+               switch (obj.datatype) {
+                       case 'timestamp':
+                               var cal_popup = $('calendar-widget');
+
+                               while (cal_popup.firstChild) cal_popup.removeChild(cal_popup.lastChild);
+                               var calendar = new Calendar(
+                                       0,
+                                       obj.op_value.object,
+                                       function (cal,date) { return timestampSetDate(obj,cal,date) },
+                                       function (cal) { cal_popup.hidePopup(); cal.destroy(); return true; }
+                               );
+
+                               var format = OILS_RPT_TRANSFORMS[obj.transform].cal_format || '%Y-%m-%d';
+
+                               calendar.setDateFormat(format);
+                               calendar.create(cal_popup);
+
+                               targetCmd.menu = 'calendar-widget';
+
+                               break;
+
+                       case 'bool':
+
+                               function __bool_value_event_handler () {
+                                       var state, answer;
+
+                                       try {
+                                               // get a reference to the prompt service component.
+                                               var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+                                                                   .getService(Components.interfaces.nsIPromptService);
+
+                                               // set the buttons that will appear on the dialog. It should be
+                                               // a set of constants multiplied by button position constants. In this case,
+                                               // three buttons appear, Save, Cancel and a custom button.
+                                               var flags=promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_0 +
+                                                       promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_1 +
+                                                       promptService.BUTTON_TITLE_CANCEL * promptService.BUTTON_POS_2;
+
+                                               // display the dialog box. The flags set above are passed
+                                               // as the fourth argument. The next three arguments are custom labels used for
+                                               // the buttons, which are used if BUTTON_TITLE_IS_STRING is assigned to a
+                                               // particular button. The last two arguments are for an optional check box.
+                                               answer = promptService.select(
+                                                       window,
+                                                       "Boolean Value",
+                                                       "Select the value, or cancel:",
+                                                       2, ["True", "False"], state
+                                               );
+                                       } catch (e) {
+                                               answer = true;
+                                               state = confirm("Click OK for TRUE and Cancel for FALSE.");
+                                               state ? state = 0 : state = 1;
+                                       }
+
+                                       if (answer) {
+                                               if (state) {
+                                                       obj.op_value.value = 'f';
+                                                       obj.op_value.label = 'False';
+                                               } else {
+                                                       obj.op_value.value = 't';
+                                                       obj.op_value.label = 'True';
+                                               }
+                                       }
+
+                                       targetCmd.removeEventListener( 'command', __bool_value_event_handler, true );
+                                       renderSources(true);
+                                       tree.view.selection.select( item_pos );
+                                       return true;
+                               }
+
+                               __handler_cache = __bool_value_event_handler;
+                               targetCmd.addEventListener( 'command', __bool_value_event_handler, true );
+
+                               break;
+
+                       default:
+
+
+                               var promptstring = "Field does not match one of list (comma separated):";
+
+                               switch (obj.op) {
+                                       case 'not between':
+                                               promptstring = "Field value is not between (comma separated):";
+                                               break;
+
+                                       case 'between':
+                                               promptstring = "Field value is between (comma separated):";
+                                               break;
+
+                                       case 'not in':
+                                               promptstring = "Field does not match one of list (comma separated):";
+                                               break;
+
+                                       case 'in':
+                                               promptstring = "Field matches one of list (comma separated):";
+                                               break;
+
+                                       default:
+                                               promptstring = "Value " + obj.op_label + ":";
+                                               break;
+                               }
+
+                               function __default_value_event_handler () {
+                                       var _v;
+                                       if (_v  = prompt( promptstring, obj.op_value.value || '' )) {
+                                               switch (obj.op) {
+                                                       case 'between':
+                                                       case 'not between':
+                                                       case 'not in':
+                                                       case 'in':
+                                                               obj.op_value.value = _v.split(/\s*,\s*/);
+                                                               break;
+
+                                                       default:
+                                                               obj.op_value.value = _v;
+                                                               break;
+                                               }
+
+                                               obj.op_value.label = '"' + obj.op_value.value + '"';
+                                       }
+
+                                       targetCmd.removeEventListener( 'command', __default_value_event_handler, true );
+                                       renderSources(true);
+                                       tree.view.selection.select( item_pos );
+                                       return true;
+                               }
+
+                               __handler_cache = __default_value_event_handler;
+                               targetCmd.addEventListener( 'command', __default_value_event_handler, true );
+
+                               break;
+               }
+       }
+
+       return true;
+}
+
+function populateOperatorContext () {
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var items = getSelectedItems(tree);
+
+       var dtypes = [];
+       for (var i in items) {
+               var item = items[i];
+               dtypes.push( item.getAttribute('datatype') );
+       }
+
+       var menu = $(tabname + '_op_menu');
+       while (menu.firstChild) menu.removeChild(menu.lastChild);
+
+       for (var i in OILS_RPT_FILTERS) {
+               var o = OILS_RPT_FILTERS[i];
+               menu.appendChild(
+                       createMenuItem(
+                               { label : o.label,
+                                 onmouseup : "changeOperator({op:'"+i+"',label:'"+o.label+"'})"
+                               }
+                       )
+               );
+               if (o.labels) {
+                       var keys = getKeys(o.labels);
+                       for ( var k in keys ) {
+                               var key = keys[k];
+                               if ( grep(function(x){return key==x},dtypes).length ) {
+                                       menu.appendChild(
+                                               createMenuItem(
+                                                       { label : o.labels[key],
+                                                         onmouseup : "changeOperator({op:'"+i+"',label:'"+o.labels[key]+"'});"
+                                                       }
+                                               )
+                                       );
+                               }
+                       }
+               }
+       }
+}
+
+function populateTransformContext () {
+
+       var active_tab = filterByAttribute(
+               $('used-source-fields-tabbox').getElementsByTagName('tab'),
+               'selected',
+               'true'
+       )[0];
+
+       var tabname = active_tab.getAttribute('id');
+
+       var tabpanel = $( tabname + 'panel' );
+       var tree = tabpanel.getElementsByTagName('tree')[0];
+       var items = getSelectedItems(tree);
+
+       var transforms = {};
+       for (var i in items) {
+               var item = items[i];
+               var dtype = item.getAttribute('datatype');
+               var item_transforms = getTransforms({ datatype : dtype });
+
+               for (var j in item_transforms) {
+                       transforms[item_transforms[j]] = OILS_RPT_TRANSFORMS[item_transforms[j]];
+                       transforms[item_transforms[j]].name = item_transforms[j];
+               }
+       }
+
+       var transformList = [];
+       for (var i in transforms) {
+               transformList.push( transforms[i] );
+       }
+
+       transformList.sort( sortHashLabels );
+
+       var menu = $(tabname + '_trans_menu');
+       while (menu.firstChild) menu.removeChild(menu.lastChild);
+
+       for (var i in transformList) {
+               var t = transformList[i];
+
+               if (tabname.match(/filter/)) {
+                       if (tabname.match(/^agg/)) {
+                               if (!t.aggregate) continue;
+                       } else {
+                               if (t.aggregate) continue;
+                       }
+               }
+
+               menu.appendChild(
+                       createMenuItem(
+                               { aggregate : t.aggregate,
+                                 name : t.name,
+                                 alias : t.label,
+                                 label : t.label,
+                                 params : t.params,
+                                 onmouseup : "alterColumnTransform('"+t.name+"')"
+                               }
+                       )
+               );
+       }
+
+       menu.parentNode.setAttribute('disabled','false');
+       if (menu.childNodes.length == 0) {
+               menu.parentNode.setAttribute('disabled','true');
+       }
+}
+
+
+function renderSources (selected) {
+
+       var tree = $('used-sources');
+       var sources = $('used-sources-treetop');
+       var tabs = $('used-source-fields-tabbox').getElementsByTagName('tab');
+
+       if (!selected) {
+               while (sources.firstChild) sources.removeChild(sources.lastChild);
+       } else {
+               selected = getSelectedItems(tree);
+               if (!selected.length) {
+                       selected = undefined;
+                       while (sources.firstChild) sources.removeChild(sources.lastChild);
+               }
+       }
+
+       for (var j = 0; j < tabs.length; j++) {
+               var tab = tabs[j];
+               var tabname = tab.getAttribute('id')
+               var tabpanel = $( tab.getAttribute('id') + 'panel' );
+               var fieldtree = tabpanel.getElementsByTagName('treechildren')[0];
+
+               while (fieldtree.firstChild) fieldtree.removeChild(fieldtree.lastChild);
+       }
+
+       for (var relation_alias in rpt_rel_cache) {
+               if (!rpt_rel_cache[relation_alias].fields) continue;
+
+               if (selected) {
+                       if (
+                               !grep(
+                                       function (x) {
+                                               return x.getAttribute('relation') == relation_alias;
+                                       },
+                                       selected
+                               ).length
+                       ) continue;
+               } else {
+
+                       $('used-sources-treetop').appendChild(
+                               createTreeItem(
+                                       { relation : rpt_rel_cache[relation_alias].alias,
+                                         idlclass : rpt_rel_cache[relation_alias].idlclass,
+                                         reltype  : rpt_rel_cache[relation_alias].reltype,
+                                         path     : rpt_rel_cache[relation_alias].path,
+                                       },
+                                       createTreeRow(
+                                               {},
+                                               createTreeCell({ label : rpt_rel_cache[relation_alias].label }),
+                                               createTreeCell({ label : rpt_rel_cache[relation_alias].table }),
+                                               createTreeCell({ label : rpt_rel_cache[relation_alias].alias }),
+                                               createTreeCell({ label : rpt_rel_cache[relation_alias].reltype })
+                                       )
+                               )
+                       );
+               }
+
+               for each (var tabname in ['filter_tab','aggfilter_tab']) {
+                       tabpanel = $( tabname + 'panel' );
+                       fieldtree = tabpanel.getElementsByTagName('treechildren')[0];
+
+                       for (var colname in rpt_rel_cache[relation_alias].fields[tabname]) {
+                               with (rpt_rel_cache[relation_alias].fields[tabname][colname]) {
+                                       fieldtree.appendChild(
+                                               createTreeItem(
+                                                       { relation : relation_alias, datatype : datatype },
+                                                       createTreeRow(
+                                                               {},
+                                                               createTreeCell({ label : alias }),
+                                                               createTreeCell({ label : colname }),
+                                                               createTreeCell({ label : datatype }),
+                                                               createTreeCell({ label : transform_label }),
+                                                               createTreeCell({ label : transform })
+                                                       )
+                                               )
+                                       );
+
+                                       fieldtree.lastChild.firstChild.appendChild(
+                                               createTreeCell({ op : op, label : op_label })
+                                       );
+
+                                       if (op_value.value != undefined) {
+                                               fieldtree.lastChild.firstChild.appendChild(
+                                                       createTreeCell({ label : op_value.label })
+                                               );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       tabpanel = $( 'dis_tabpanel' );
+       fieldtree = tabpanel.getElementsByTagName('treechildren')[0];
+       for each (var order in rpt_rel_cache.order_by) {
+
+               if (selected) {
+                       if (
+                               !grep(
+                                       function (x) {
+                                               return x.getAttribute('relation') == order.relation;
+                                       },
+                                       selected
+                               ).length
+                       ) continue;
+               }
+
+               with (rpt_rel_cache[order.relation].fields.dis_tab[order.field]) {
+                       fieldtree.appendChild(
+                               createTreeItem(
+                                       { relation : order.relation, datatype : datatype },
+                                       createTreeRow(
+                                               {},
+                                               createTreeCell({ label : alias }),
+                                               createTreeCell({ label : colname }),
+                                               createTreeCell({ label : datatype }),
+                                               createTreeCell({ label : transform_label }),
+                                               createTreeCell({ label : transform })
+                                       )
+                               )
+                       );
+
+                       fieldtree.lastChild.firstChild.appendChild(
+                               createTreeCell({ op : op, label : op_label })
+                       );
+
+                       if (op_value.value != undefined) {
+                               fieldtree.lastChild.firstChild.appendChild(
+                                       createTreeCell({ label : op_value.label })
+                               );
+                       }
+               }
+       }
+}
+
+var param_count;
+var tab_use = {
+       dis_tab       : 'select',
+       filter_tab    : 'where',
+       aggfilter_tab : 'having',
+};
+
+
+function save_template () {
+       param_count = 0;
+
+       var template = {
+               version    : 2,
+               core_class : $('sources-treetop').getElementsByTagName('treeitem')[0].getAttribute('idlclass'),
+               select     : [],
+               from       : {},
+               where      : [],
+               having     : [],
+               order_by   : []
+       };
+
+       for (var relname in rpt_rel_cache) {
+               var relation = rpt_rel_cache[relname];
+               if (!relation.fields) continue;
+
+               // first, add the where and having clauses (easier)
+               for each (var tab_name in [ 'filter_tab', 'aggfilter_tab' ]) {
+                       var tab = relation.fields[tab_name];
+                       for (var field in tab) {
+                               fleshTemplateField( template, relation, tab_name, field );
+                       }
+               }
+
+               // now the from clause
+               fleshFromPath( template, relation );
+       }
+
+       // and now for select (based on order_by)
+       for each (var order in rpt_rel_cache.order_by)
+               fleshTemplateField( template, rpt_rel_cache[order.relation], 'dis_tab', order.field );
+
+       template.rel_cache = rpt_rel_cache;
+
+       //prompt( 'template', js2JSON( template ) );
+
+       // and the saving throw ...
+       var cgi = new CGI();
+       var session = cgi.param('ses');
+       fetchUser( session );
+
+       var tmpl = new rt();
+       tmpl.name( $('template-name').value );
+       tmpl.description( $('template-description').value );
+       tmpl.owner(USER.id());
+       tmpl.folder(cgi.param('folder'));
+       tmpl.data(js2JSON(template));
+
+       if(!confirm('Name : '+tmpl.name() + '\nDescription: ' + tmpl.description()+'\nSave Template?'))
+               return;
+
+       var req = new Request('open-ils.reporter:open-ils.reporter.template.create', session, tmpl);
+       req.request.alertEvent = false;
+       req.callback(
+               function(r) {
+                       var res = r.getResultObject();
+                       if(checkILSEvent(res)) {
+                               alertILSEvent(res);
+                       } else {
+                               if( res && res != '0' ) {
+                                       confirm('Template ' + tmpl.name() + ' was successfully saved.');
+                                       _l('../oils_rpt.xhtml');
+                               }
+                       }
+               }
+       );
+
+       req.send();
+}
+
+function fleshFromPath ( template, rel ) {
+       var table_path = rel.path.split( /\./ );
+       if (table_path.length > 1 || rel.path.indexOf('-') > -1) table_path.push( rel.idlclass );
+
+       var prev_type = '';
+       var prev_link = '';
+       var current_path = '';
+       var current_obj = template.from;
+       var link;
+       while (link = table_path.shift()) {
+               if (current_path) current_path += '-';
+               current_path += link;
+
+               var leaf = table_path.length == 0 ? true : false;
+
+               current_obj.path = current_path;
+               current_obj.table = getIDLClass( link.split(/-/)[0] ).getAttributeNS( persistNS, 'tablename' );
+
+               if (prev_link != '') {
+                       var prev_class = getIDLClass( prev_link.split(/-/)[0] );
+                       var prev_field = prev_link.split(/-/)[1];
+
+                       var current_link = getIDLLink( prev_class, prev_field );
+                       current_obj.key = current_link.getAttribute('key');
+
+                       if (
+                               current_link.getAttribute('reltype') != 'has_a' ||
+                               prev_type == 'left' ||
+                               rel.reltype != 'has_a'
+                       ) current_obj.type = 'left';
+
+                       prev_type = current_obj.type; 
+               }
+
+               if (leaf) {
+                       current_obj.label = rel.label;
+                       current_obj.alias = rel.alias;
+                       current_obj.idlclass = rel.idlclass;
+                       current_obj.template_path = rel.path;
+               } else {
+                       var current_class = getIDLClass( link.split(/-/)[0] );
+                       var join_field = link.split(/-/)[1];
+                       var join_link = getIDLLink(current_class, join_field);
+
+                       if (join_link.getAttribute('reltype') != 'has_a') {
+                               var fields_el = current_class.getElementsByTagName('fields')[0];
+                               join_field = fields_el.getAttributeNS(persistNS, 'primary') + '-' + join_link.getAttribute('key');
+                       }
+
+                       current_obj.alias = hex_md5( current_path ) ;
+                       join_field += '-' + current_obj.alias;
+                       if (table_path.length == 1) join_field += '-' + rel.alias;
+
+                       if (!current_obj.join) current_obj.join = {};
+                       if (!current_obj.join[join_field]) current_obj.join[join_field] = {};
+
+                       if (current_obj.type == 'left') current_obj.join[join_field].type = 'left';
+
+                       current_obj = current_obj.join[join_field];
+               }
+
+               prev_link = link;
+       }
+
+
+}
+
+function fleshTemplateField ( template, rel, tab_name, field ) {
+
+       if (!rel.fields[tab_name] || !rel.fields[tab_name][field]) return;
+
+       var tab = rel.fields[tab_name];
+
+       var table_path = rel.path.split( /\./ );
+       if (table_path.length > 1 || rel.path.indexOf('-') > -1)
+               table_path.push( rel.idlclass );
+
+       table_path.push( field );
+
+       var field_path = table_path.join('-');
+
+       var element = {
+               alias : tab[field].alias,
+               column :
+                       { colname : field,
+                         transform : tab[field].transform,
+                         transform_label : tab[field].transform_label
+                       },
+               path : field_path,
+               relation : rel.alias
+       };
+
+       if (tab_name.match(/filter/)) {
+               element.condition = {};
+               element.condition[tab[field].op] =
+                       tab[field].op_value.value ?
+                               tab[field].op_value.value :
+                               '::P' + param_count++;
+       }
+
+       template[tab_use[tab_name]].push(element);
+}
+
diff --git a/Open-ILS/web/reports/xul/template_builder.xul b/Open-ILS/web/reports/xul/template_builder.xul
new file mode 100644 (file)
index 0000000..434066e
--- /dev/null
@@ -0,0 +1,315 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="reporter.css" type="text/css"?>
+<?xml-stylesheet href="/opac/common/js/jscalendar/calendar-brown.css" type="text/css" ?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xhtml="http://www.w3.org/1999/xhtml" onload="loadIDL()">
+
+<script src='/opac/common/js/utils.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/config.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/CGI.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/JSON.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/fmall.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/fmgen.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/Cookies.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/opac_utils.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/OrgTree.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/org_utils.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/RemoteRequest.js' type="application/x-javascript; e4x=1"/>
+<script src='/opac/common/js/md5.js' type="application/x-javascript; e4x=1"/>
+
+<script src="../adminlib.js" type="application/x-javascript; e4x=1"/>
+
+<script src="utilities.js" type="application/x-javascript; e4x=1"/>
+<script src="xulbuilder.js" type="application/x-javascript; e4x=1"/>
+<script src="source-setup.js" type="application/x-javascript; e4x=1"/>
+<script src="source-browse.js" type="application/x-javascript; e4x=1"/>
+<script src="template-config.js" type="application/x-javascript; e4x=1"/>
+<script src="transforms.js" type="application/x-javascript; e4x=1"/>
+<script src="operators.js" type="application/x-javascript; e4x=1"/>
+
+<script type="application/x-javascript; e4x=1" src="/opac/common/js/jscalendar/calendar.js"/>
+<script type="application/x-javascript; e4x=1" src="/opac/common/js/jscalendar/lang/calendar-en.js"/>
+<script type="application/x-javascript; e4x=1" src="/opac/common/js/jscalendar/calendar-setup.js"/>
+
+<groupbox flex="1">
+       <caption label="Database Source Browser"/>
+       <hbox flex="1">
+               <hbox flex="1">
+                       <vbox flex="1">
+                               <menulist label="Sources" popup="source-menu"/>
+                               <tree
+                                       id="idl-browse-tree"
+                                       flex="2"
+                                       onclick="sourceTreeHandler(event)"
+                                       ondblclick="sourceTreeHandlerDblClick(event)"
+                               >
+                                       <treecols>
+                                               <treecol primary="true" label="Source Name" flex="1"/>
+                                       </treecols>
+                                       <treechildren id="sources-treetop" alternatingbackground="true" />
+                               </tree>
+                       </vbox>
+               </hbox>
+
+               <splitter id="rtp-browse-splitter" collapse="before" persist="state hidden"><grippy/></splitter>
+
+               <hbox flex="2">
+                       <vbox flex="1">
+                               <hbox>
+                                       <label control="path-label" value="Source Specifier:"/>
+                                       <textbox id="path-label" flex="1"/>
+                               </hbox>
+                               <hbox flex="1">
+                                       <hbox flex="3">
+                                               <vbox flex="1">
+                                                       <tree
+                                                               id="class-view"
+                                                               flex="3"
+                                                               onclick="detailTreeHandler()"
+                                                               ondblclick="addReportAtoms()"
+                                                               enableColumnDrag="true"
+                                                       >
+                                                               <treecols>
+                                                                       <treecol label="Field Name" flex="1"/>
+                                                                       <treecol label="Data Type" flex="0"/>
+                                                               </treecols>
+                                                               <treechildren id="class-treetop" alternatingbackground="true" />
+                                                       </tree>
+                                               </vbox>
+                                       </hbox>
+
+                                       <splitter><grippy/></splitter>
+
+                                       <hbox flex="2">
+                                               <vbox flex="1">
+                                                       <tree
+                                                               id="trans-view"
+                                                               flex="1"
+                                                               seltype="single"
+                                                               onclick="transformSelectHandler()"
+                                                               ondblclick="addReportAtoms()"
+                                                               enableColumnDrag="true"
+                                                       >
+                                                               <treecols>
+                                                                       <treecol label="Field Transform" flex="1"/>
+                                                                       <treecol label="Params" flex="0" hidden="true"/>
+                                                                       <treecol label="Applicable Datatypes" flex="1" hidden="true"/>
+                                                                       <treecol label="Output Type" flex="1" />
+                                                               </treecols>
+                                                               <treechildren id="trans-treetop" alternatingbackground="true" />
+                                                       </tree>
+                                               </vbox>
+                                       </hbox>
+                               </hbox>
+                               <hbox>
+                                       <spacer flex="1"/>
+                                       <button label="Add Selected Fields" id="source-add" oncommand="addReportAtoms()"/>
+                               </hbox>
+                       </vbox>
+               </hbox>
+       </hbox>
+</groupbox>
+
+<splitter style="margin:3px" id="rtp-browse-build-splitter" collapse="before" persist="state hidden"><grippy/></splitter>
+
+
+<groupbox flex="1" orient="horizontal">
+       <caption label="Template Configuration"/>
+
+       <hbox flex="1">
+               <vbox flex="1">
+                       <hbox>
+                       <vbox>
+                               <label control="template-name" value="Name:" style="height:2em"/>
+                               <label control="template-description" value="Description:"/>
+                       </vbox>
+                       <vbox flex="1">
+                               <textbox id="template-name" flex="1"/>
+                               <textbox id="template-description" multiline="true" flex="1" style="max-height:3em"/>
+                       </vbox>
+                       <vbox pack="end">
+                               <button onclick="save_template();" label="Save"/>
+                       </vbox>
+                       </hbox>
+
+                       <hbox flex="1">
+                               <tabbox flex="2" id="used-source-fields-tabbox">
+                                       <tabs>
+                                               <tab
+                                                       id="dis_tab"
+                                                       label="Displayed Fields"
+                                                       onclick="transformSelectHandler(true);"
+                                               />
+                                               <tab
+                                                       id="filter_tab"
+                                                       label="Base Filters"
+                                                       onclick="transformSelectHandler(true);"
+                                               />
+                                               <tab
+                                                       id="aggfilter_tab"
+                                                       label="Aggregate Filters"
+                                                       disabled="true"
+                                                       onclick="transformSelectHandler(true);"
+                                               />
+                                               <!--
+                                               <tab
+                                                       id="order_tab"
+                                                       label="Field Order"
+                                                       onclick="transformSelectHandler(true);"
+                                               />
+                                               -->
+                                       </tabs>
+
+                                       <tabpanels flex="1">
+                                               <tabpanel id="dis_tabpanel" orient="vertical">
+                                                       <vbox flex="1">
+                                                               <hbox flex="1">
+                                                                       <tree
+                                                                               id="dis-col-view"
+                                                                               flex="1"
+                                                                               seltype="single"
+                                                                               ondblclick="alterColumnLabel()"
+                                                                               onselect="populateTransformContext()"
+                                                                               enableColumnDrag="true"
+                                                                       >
+                                                                               <treecols>
+                                                                                       <treecol label="Display Name" flex="3"/>
+                                                                                       <treecol label="Field Name" hidden="true" flex="1"/>
+                                                                                       <treecol label="Data Type" flex="1"/>
+                                                                                       <treecol label="Field Transform" flex="1"/>
+                                                                                       <treecol label="Field Transform Type" hidden="true" flex="1"/>
+                                                                               </treecols>
+                                                                               <treechildren id="dis-col-treetop" alternatingbackground="true" />
+                                                                       </tree>
+                                                               </hbox>
+                                                               <hbox pack="center">
+                                                                       <button label="Alter Display Header" oncommand="alterColumnLabel()"/>
+                                                                       <button type="menu" label="Change Transform">
+                                                                               <menupopup id='dis_tab_trans_menu'/>
+                                                                       </button>
+                                                                       <spacer flex="1"/>
+                                                                       <button label="Move Up" oncommand="changeDisplayOrder('u')"/>
+                                                                       <button label="Move Down" oncommand="changeDisplayOrder('d')"/>
+                                                                       <spacer flex="1"/>
+                                                                       <button label="Remove Selected Field" oncommand="removeReportAtom()"/>
+                                                               </hbox>
+                                                       </vbox>
+                                               </tabpanel>
+
+                                               <tabpanel id="filter_tabpanel" orient="vertical">
+                                                       <vbox flex="1">
+                                                               <hbox flex="1">
+                                                                       <tree
+                                                                               id="filter-col-view"
+                                                                               flex="1"
+                                                                               seltype="single"
+                                                                               onselect="populateTransformContext();populateOperatorContext();changeTemplateFilterValue();"
+                                                                               enableColumnDrag="true"
+                                                                       >
+                                                                               <treecols>
+                                                                                       <treecol label="Filter Field" flex="2"/>
+                                                                                       <treecol label="Field Name" hidden="true" flex="1"/>
+                                                                                       <treecol label="Data Type" hidden="true" flex="1"/>
+                                                                                       <treecol label="Field Transform" flex="1"/>
+                                                                                       <treecol label="Field Transform Type" hidden="true" flex="1"/>
+                                                                                       <treecol label="Operator" flex="1"/>
+                                                                                       <treecol label="Value" flex="1"/>
+                                                                               </treecols>
+                                                                               <treechildren id="filter-col-treetop" alternatingbackground="true" />
+                                                                       </tree>
+                                                               </hbox>
+                                                               <hbox pack="center">
+                                                                       <button type="menu" label="Change Transform">
+                                                                               <menupopup id='filter_tab_trans_menu'/>
+                                                                       </button>
+                                                                       <button type="menu" label="Change Operator">
+                                                                               <menupopup id='filter_tab_op_menu'/>
+                                                                       </button>
+                                                                       <button label="Change value" command="filter_tab_value_action"/>
+                                                                       <button label="Remove value" oncommand="removeTemplateFilterValue()"/>
+                                                                       <spacer flex="1"/>
+                                                                       <button label="Remove Selected Fields" oncommand="removeReportAtom()"/>
+                                                               </hbox>
+                                                       </vbox>
+                                               </tabpanel>
+
+                                               <tabpanel id="aggfilter_tabpanel" orient="vertical">
+                                                       <vbox flex="1">
+                                                               <hbox flex="1">
+                                                                       <tree
+                                                                               id="aggfilter-col-view"
+                                                                               flex="1"
+                                                                               seltype="single"
+                                                                               onselect="populateTransformContext();populateOperatorContext();changeTemplateFilterValue();"
+                                                                               enableColumnDrag="true"
+                                                                       >
+                                                                               <treecols>
+                                                                                       <treecol label="Filter Field" flex="2"/>
+                                                                                       <treecol label="Field Name" hidden="true" flex="1"/>
+                                                                                       <treecol label="Data Type" hidden="true" flex="1"/>
+                                                                                       <treecol label="Field Transform" flex="1"/>
+                                                                                       <treecol label="Field Transform Type" hidden="true" flex="1"/>
+                                                                                       <treecol label="Operator" flex="1"/>
+                                                                                       <treecol label="Value" flex="1"/>
+                                                                               </treecols>
+                                                                               <treechildren id="aggfilter-col-treetop" alternatingbackground="true" />
+                                                                       </tree>
+                                                               </hbox>
+                                                               <hbox pack="center">
+                                                                       <button type="menu" label="Change Transform">
+                                                                               <menupopup id='aggfilter_tab_trans_menu'/>
+                                                                       </button>
+                                                                       <button type="menu" label="Change Operator">
+                                                                               <menupopup id='aggfilter_tab_op_menu'/>
+                                                                       </button>
+                                                                       <button label="Change value" command="aggfilter_tab_value_action"/>
+                                                                       <button label="Remove value" oncommand="removeTemplateFilterValue()"/>
+                                                                       <spacer flex="1"/>
+                                                                       <button label="Remove Selected Fields" oncommand="removeReportAtom()"/>
+                                                               </hbox>
+                                                       </vbox>
+                                               </tabpanel>
+                                       </tabpanels>
+                               </tabbox>
+                       </hbox>
+               </vbox>
+       </hbox>
+
+       <splitter id="rtp-build-splitter" collapse="after" persist="state hidden"><grippy/></splitter>
+
+       <hbox flex="3">
+               <tree
+                       id="used-sources"
+                       flex="1"
+                       onclick="renderSources(true)"
+                       ondblclick="changeTemplateFilterValue()"
+                       enableColumnDrag="true"
+               >
+                       <treecols>
+                               <treecol label="Source Specifier" flex="2"/>
+                               <treecol label="Table Name" flex="1" hidden="true"/>
+                               <treecol label="SQL Alias" flex="1" hidden="true"/>
+                               <treecol label="Relationship" flex="1" hidden="true"/>
+                       </treecols>
+                       <treechildren id="used-sources-treetop" alternatingbackground="true" />
+               </tree>
+       </hbox>
+
+</groupbox>
+
+<commandset>
+       <command id='filter_tab_value_action'/>
+       <command id='aggfilter_tab_value_action'/>
+</commandset>
+
+<popupset>
+       <popup id="source-menu" position="after_start"/>
+       <popup
+               id="calendar-widget"
+               position="before_start"
+       />
+</popupset>
+
+</window>
+
diff --git a/Open-ILS/web/reports/xul/transforms.js b/Open-ILS/web/reports/xul/transforms.js
new file mode 100644 (file)
index 0000000..1103789
--- /dev/null
@@ -0,0 +1,233 @@
+var OILS_RPT_DTYPE_STRING = 'text';
+var OILS_RPT_DTYPE_MONEY = 'money';
+var OILS_RPT_DTYPE_BOOL = 'bool';
+var OILS_RPT_DTYPE_INT = 'int';
+var OILS_RPT_DTYPE_ID = 'id';
+var OILS_RPT_DTYPE_FLOAT = 'float';
+var OILS_RPT_DTYPE_TIMESTAMP = 'timestamp';
+
+var OILS_RPT_DTYPE_ALL = [OILS_RPT_DTYPE_STRING,OILS_RPT_DTYPE_MONEY,OILS_RPT_DTYPE_INT,OILS_RPT_DTYPE_ID,OILS_RPT_DTYPE_FLOAT,OILS_RPT_DTYPE_TIMESTAMP,OILS_RPT_DTYPE_BOOL];
+var OILS_RPT_DTYPE_NOT_ID = [OILS_RPT_DTYPE_STRING,OILS_RPT_DTYPE_MONEY,OILS_RPT_DTYPE_INT,OILS_RPT_DTYPE_FLOAT,OILS_RPT_DTYPE_TIMESTAMP];
+var OILS_RPT_DTYPE_NOT_BOOL = [OILS_RPT_DTYPE_STRING,OILS_RPT_DTYPE_MONEY,OILS_RPT_DTYPE_INT,OILS_RPT_DTYPE_FLOAT,OILS_RPT_DTYPE_TIMESTAMP,OILS_RPT_DTYPE_ID];
+
+var OILS_RPT_TRANSFORMS = {
+       Bare : {
+               datatype : OILS_RPT_DTYPE_ALL,
+               label : 'Raw Data'
+       },
+
+       first : {
+               datatype : OILS_RPT_DTYPE_NOT_ID,
+               label : 'First Value'
+       },
+
+       last : {
+               datatype : OILS_RPT_DTYPE_NOT_ID,
+               label : 'Last Value'
+       },
+
+       count : {
+               datatype : OILS_RPT_DTYPE_NOT_BOOL,
+               aggregate : true,
+               label :  'Count'
+       },
+
+       count_distinct : {
+               datatype : OILS_RPT_DTYPE_NOT_BOOL,
+               aggregate : true,
+               label : 'Count Distinct'
+       },
+
+       min : {
+               datatype : OILS_RPT_DTYPE_NOT_ID,
+               aggregate : true,
+               label : 'Min'
+       },
+
+       max : {
+               datatype : OILS_RPT_DTYPE_NOT_ID,
+               aggregate : true,
+               label : 'Max'
+       },
+
+       /* string transforms ------------------------- */
+
+       substring : {
+               datatype : [ OILS_RPT_DTYPE_STRING ],
+               params : 2,
+               label : 'Substring'
+       },
+
+       lower : {
+               datatype : [ OILS_RPT_DTYPE_STRING ],
+               label : 'Lower case'
+       },
+
+       upper : {
+               datatype : [ OILS_RPT_DTYPE_STRING ],
+               label : 'Upper case'
+       },
+
+       /* timestamp transforms ----------------------- */
+       dow : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Day of Week',
+               cal_format : '%w',
+               regex : /^[0-6]$/
+       },
+       dom : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Day of Month',
+               cal_format : '%e',
+               regex : /^[0-9]{1,2}$/
+       },
+
+       doy : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Day of Year',
+               cal_format : '%j',
+               regex : /^[0-9]{1,3}$/
+       },
+
+       woy : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Week of Year',
+               cal_format : '%U',
+               regex : /^[0-9]{1,2}$/
+       },
+
+       moy : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Month of Year',
+               cal_format : '%m',
+               regex : /^\d{1,2}$/
+       },
+
+       qoy : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Quarter of Year',
+               regex : /^[1234]$/
+       }, 
+
+       hod : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Hour of day',
+               cal_format : '%H',
+               regex : /^\d{1,2}$/
+       }, 
+
+       date : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Date',
+               regex : /^\d{4}-\d{2}-\d{2}$/,
+               hint  : 'YYYY-MM-DD',
+               cal_format : '%Y-%m-%d',
+               input_size : 10
+       },
+
+       month_trunc : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Year + Month',
+               regex : /^\d{4}-\d{2}$/,
+               hint  : 'YYYY-MM',
+               cal_format : '%Y-%m',
+               input_size : 7
+       },
+
+       year_trunc : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Year',
+               regex : /^\d{4}$/,
+               hint  : 'YYYY',
+               cal_format : '%Y',
+               input_size : 4
+       },
+
+       hour_trunc : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Hour',
+               regex : /^\d{2}$/,
+               hint  : 'HH',
+               cal_format : '%Y-%m-$d %H',
+               input_size : 2
+       },
+
+       day_name : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               cal_format : '%A',
+               label : 'Day Name'
+       }, 
+
+       month_name : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               cal_format : '%B',
+               label : 'Month Name'
+       },
+       age : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Age'
+       },
+
+       months_ago : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Months ago'
+       },
+
+       quarters_ago : {
+               datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+               label : 'Quarters ago'
+       },
+
+       /* int  / float transforms ----------------------------------- */
+       sum : {
+               datatype : [ OILS_RPT_DTYPE_INT, OILS_RPT_DTYPE_FLOAT, OILS_RPT_DTYPE_MONEY ],
+               label : 'Sum',
+               aggregate : true
+       }, 
+
+       average : {
+               datatype : [ OILS_RPT_DTYPE_INT, OILS_RPT_DTYPE_FLOAT, OILS_RPT_DTYPE_MONEY ],
+               label : 'Average',
+               aggregate : true
+       },
+
+       round : {
+               datatype : [ OILS_RPT_DTYPE_INT, OILS_RPT_DTYPE_FLOAT ],
+               label : 'Round',
+       },
+
+       'int' : {
+               datatype : [ OILS_RPT_DTYPE_FLOAT ],
+               label : 'Drop trailing decimals'
+       }
+}
+
+function getTransforms(args) {
+       var dtype = args.datatype;
+       var agg = args.aggregate;
+       var tforms = OILS_RPT_TRANSFORMS;
+       var nonagg = args.non_aggregate;
+
+       var keys = getKeys(OILS_RPT_TRANSFORMS)
+       var tforms = [];
+
+       for( var i = 0; i < keys.length; i++ ) {
+               var key = keys[i];
+               var obj = OILS_RPT_TRANSFORMS[key];
+               if( agg && !nonagg && !obj.aggregate ) continue;
+               if( !agg && nonagg && obj.aggregate ) continue;
+               if( !dtype && obj.datatype.length > 0 ) continue;
+               if( dtype && obj.datatype.length > 0 && transformIsForDatatype(key,dtype).length == 0 ) continue;
+               tforms.push(key);
+       }
+
+       return tforms;
+}
+
+
+function transformIsForDatatype(tform, dtype) {
+       var obj = OILS_RPT_TRANSFORMS[tform];
+       return grep(function(d) { return (d == dtype) }, obj.datatype);
+}
+
+
diff --git a/Open-ILS/web/reports/xul/utilities.js b/Open-ILS/web/reports/xul/utilities.js
new file mode 100644 (file)
index 0000000..bfefacf
--- /dev/null
@@ -0,0 +1,83 @@
+function $ () {
+       var elements = new Array();
+
+       for (var i = 0; i < arguments.length; i++) {
+               var element = arguments[i];
+
+               if (typeof element == 'string')
+                       element = document.getElementById(element) || undefined;
+
+               if (arguments.length == 1)
+                       return element;
+
+               elements.push( element );
+       }
+
+       return elements;
+}
+
+function _l(l) { location.href = l + location.search; }
+
+function map (func, list) {
+        var ret = [];
+        for (var i = 0; i < list.length; i++) ret.push(func(list[i]));
+        return ret;
+}
+
+function grep (func, list) {
+       var ret = [];
+       for (var i = 0; i < list.length; i++) if(func(list[i])) ret.push(list[i]);
+       return ret;
+}
+
+function getSelectedItems(tree) {
+        var start = new Object();
+        var end = new Object();
+        var numRanges = tree.view.selection.getRangeCount();
+                        
+        var itemList = [];
+        for (var t=0; t<numRanges; t++){
+                tree.view.selection.getRangeAt(t,start,end);
+                for (var v=start.value; v<=end.value; v++){
+                        itemList.push( tree.getElementsByTagName('treeitem')[v]);
+                }       
+        }               
+                
+        return itemList;
+}
+
+function findAnscestor (node, name) {
+        if (node.nodeName == name) return node;
+        if (!node.parentNode) return null;
+        return findAnscestor(node.parentNode, name);
+}       
+
+function findAnscestorStack (node, name, stack) {
+        if (node.nodeName == name) stack.push(node);
+        if (!node.parentNode) return null;
+        findAnscestorStack(node.parentNode, name, stack);
+}               
+
+function filterByAttribute(nodes,attrN,attrV) {
+        var aResponse = [];
+        for ( var i = 0; i < nodes.length; i++ ) {
+                if ( nodes[i].getAttribute(attrN) == attrV ) aResponse.push(nodes[i]);
+        }               
+        return aResponse;
+}       
+
+function filterByAttributeNS(nodes,ns,attrN,attrV) {
+        var aResponse = [];
+        for ( var i = 0; i < nodes.length; i++ ) {
+                if ( nodes[i].getAttributeNS(ns,attrN) == attrV ) aResponse.push(nodes[i]);
+        }
+        return aResponse;
+}
+
+function getKeys (hash) {
+        var k = [];
+        for (var i in hash) k.push(i);
+        return k;
+}
+
+
diff --git a/Open-ILS/web/reports/xul/xulbuilder.js b/Open-ILS/web/reports/xul/xulbuilder.js
new file mode 100644 (file)
index 0000000..5b9693a
--- /dev/null
@@ -0,0 +1,107 @@
+function createComplexHTMLElement (e, attrs, objects, text) {
+        var l = document.createElementNS('http://www.w3.org/1999/xhtml',e);
+
+        if (attrs) {
+                for (var i in attrs) l.setAttribute(i,attrs[i]);
+        }
+
+        if (objects) {
+                for ( var i in objects ) l.appendChild( objects[i] );
+        }
+
+        if (text) {
+                l.appendChild( document.createTextNode(text) )
+        }
+
+        return l;
+}
+
+function createComplexXULElement (e, attrs, objects) {
+        var l = document.createElementNS('http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul',e);
+
+        if (attrs) {
+                for (var i in attrs) {
+                        if (typeof attrs[i] == 'function') {
+                                l.addEventListener( i, attrs[i], true );
+                        } else {
+                                l.setAttribute(i,attrs[i]);
+                        }
+                }
+        }
+
+        if (objects) {
+                for ( var i in objects ) l.appendChild( objects[i] );
+        }
+
+        return l;
+}
+
+function createDescription (attrs) {
+        return createComplexXULElement('description', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createTooltip (attrs) {
+        return createComplexXULElement('tooltip', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createLabel (attrs) {
+        return createComplexXULElement('label', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createVbox (attrs) {
+        return createComplexXULElement('vbox', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createHbox (attrs) {
+        return createComplexXULElement('hbox', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createRow (attrs) {
+        return createComplexXULElement('row', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createTextbox (attrs) {
+        return createComplexXULElement('textbox', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createCheckbox (attrs) {
+        return createComplexXULElement('checkbox', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createTreeChildren (attrs) {
+        return createComplexXULElement('treechildren', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createTreeItem (attrs) {
+        return createComplexXULElement('treeitem', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createTreeRow (attrs) {
+        return createComplexXULElement('treerow', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createTreeCell (attrs) {
+        return createComplexXULElement('treecell', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createPopup (attrs) {
+        return createComplexXULElement('popup', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createMenuPopup (attrs) {
+        return createComplexXULElement('menupopup', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createMenu (attrs) {
+        return createComplexXULElement('menu', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createMenuItem (attrs) {
+        return createComplexXULElement('menuitem', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+function createMenuSeparator (attrs) {
+        return createComplexXULElement('menuseparator', attrs, Array.prototype.slice.apply(arguments, [1]) );
+}
+
+