P.V. SUPA GoodStuff integration
authorJason Etheridge <jason@esilibrary.com>
Wed, 12 Dec 2012 01:02:38 +0000 (20:02 -0500)
committerMike Rylander <mrylander@gmail.com>
Thu, 12 Sep 2013 17:50:39 +0000 (13:50 -0400)
This impliments a "Server Add-ons" module for integrating P.V. Supa's RFID
product known as GoodStuff with the Evergreen staff client.

To activate it, you should add the identifier "pv_supa_goodstuff" (without the
quotes) to the list managed by the Admin->Workstation Administration->Server
Add-ons menu action within the staff client.  You will need the
ADMIN_SERVER_ADDON_FOR_WORKSTATION permission to do this.

After doing this and clicking the Update Active Add-Ons button, the interface
will refresh and show a GoodStuff tab in the Add-on Preferences section.  Within
this tab you will have the option of specifying the hostname and port for the
GoodStuff hardware. There is also an "Enabled" setting that needs to be checked.

Currently three interfaces have been integrated:
* Circulation -> Check In Items
* Circulation -> Check Out Items (where you scan the patron barcode)
* Circulation -> Check Out Items (where you scan the item barcodes)

Each interface gets an RFID checkbox if the "Enabled" preference has been set,
that can activate/deactivate the functionality on a per interface basis.  The
checkbox states persist (i.e. are sticky).

Signed-off-by: Jason Etheridge <jason@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_config_overlay.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test1.expect [new file with mode: 0755]
Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test2.expect [new file with mode: 0755]
Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test3.expect [new file with mode: 0755]
Open-ILS/xul/staff_client/server/locale/en-US/addon/pv_supa_goodstuff.properties [new file with mode: 0644]
Open-ILS/xul/staff_client/server/skin/addon/pv_supa_goodstuff.css [new file with mode: 0644]
docs/RELEASE_NOTES_NEXT/pv_supa_goodstuff.txt [new file with mode: 0644]

diff --git a/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff.js b/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff.js
new file mode 100644 (file)
index 0000000..f2810b5
--- /dev/null
@@ -0,0 +1,802 @@
+dump('entering addon/pv_supa.js\n');
+// vim:noet:sw=4:ts=4:
+
+/*
+    Usage example:
+
+    JSAN.use('addon.pv_supa_goodstuff');
+    var goodstuff = new addon.pv_supa_goodstuff();
+    goodstuff.onData(
+        function(data) {
+            alert('Received: ' + data);
+        },
+        'ACTIVATE'
+    );
+    goodstuff.request_items();
+*/
+
+if (typeof addon == 'undefined') addon = {};
+addon.pv_supa_goodstuff = function (params) {
+    var obj = this;
+    try {
+        dump('addon: pv_supa_goodstuff() constructor\n');
+
+        const Cc = Components.classes;
+        const Ci = Components.interfaces;
+        const prefs_Cc = '@mozilla.org/preferences-service;1';
+        this.prefs = Cc[prefs_Cc].getService(Ci['nsIPrefBranch']);
+
+        JSAN.use('OpenILS.data');
+        this.data = new OpenILS.data();
+        this.data.stash_retrieve();
+
+        JSAN.use('util.error');
+        this.error = new util.error();
+
+        this.active = false;
+        if (this.prefs.prefHasUserValue('oils.addon.pvsupa.goodstuff.enabled')){
+            this.active = this.prefs.getBoolPref(
+                'oils.addon.pvsupa.goodstuff.enabled'
+            );
+        }
+        if (this.active) {
+            dump('addon: pv_supa goodstuff enabled by preference\n');
+        } else {
+            dump('addon: pv_supa goodstuff not enabled by preference\n');
+        }
+
+        if (g) {
+            if (g.checkin && this.active) {
+                this.common_ui_init();
+                this.socket_init();
+                setTimeout(
+                    function() {
+                        obj.checkin_init();
+                    }, 1000
+                );
+            }
+            if (g.checkout && this.active) {
+                this.common_ui_init();
+                this.socket_init();
+                setTimeout(
+                    function() {
+                        obj.checkout_init();
+                    }, 1000
+                );
+            }
+            if (String(location.href).match('/patron/barcode_entry.xul') && this.active) {
+                this.common_ui_init();
+                this.socket_init();
+                setTimeout(
+                    function() {
+                        obj.patron_for_checkout_init();
+                    }, 1000
+                );
+            }
+            if (g.addons_ui) {
+                this.common_ui_init();
+                this.prefs_init();
+            }
+        }
+        return this;
+
+    } catch(E) {
+        dump('addon: Error in pv_supa_goodstuff(): ' + E + '\n');
+    }
+}
+
+addon.pv_supa_goodstuff.prototype = {
+    'common_ui_init' : function() {
+        dump('addon: pv_supa common_ui_init\n');
+        var obj = this;
+        try {
+            var mc = document.createElement('messagecatalog');
+            mc.setAttribute('id','addon_pvsupa_goodstuff_strings');
+            mc.setAttribute(
+                'src',
+                '/xul/server/locale/'+LOCALE+'/addon/pv_supa_goodstuff.properties'
+            );
+            var mc_parent = $('offlineStrings')
+                ? $('offlineStrings').parentNode
+                : document.getElementsByTagName('window').item(0)
+                || document.getElementsByTagName('html').item(0);
+            mc_parent.appendChild(mc);
+
+            // We don't really need CSS here, but as an example:
+            var sss = Cc['@mozilla.org/content/style-sheet-service;1']
+                .getService(Ci.nsIStyleSheetService);
+            var ios = Cc['@mozilla.org/network/io-service;1']
+                .getService(Ci.nsIIOService);
+            var uri = ios.newURI(
+                'oils://remote/xul/server/skin/addon/pv_supa_goodstuff.css',
+                null,
+                null
+            );
+            if(!sss.sheetRegistered(uri, sss.USER_SHEET)) {
+                sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
+            }
+        } catch(E) {
+            dump('addon: pv_supa Error in common_ui_init(): ' + E + '\n');
+        }
+    },
+
+    'socket_init' : function() {
+        dump('addon: pv_supa socket_init on page ' + location.href + '\n');
+        var obj = this;
+        try {
+            if (! this.data.addon) {
+                this.data.addon = {};
+            }
+            if (! this.data.addon.pv_supa) {
+                this.data.addon.pv_supa = {};
+            }
+            if (! this.data.addon.pv_supa.goodstuff) {
+                this.data.addon.pv_supa.goodstuff = {};
+            }
+            if (! this.data.addon.pv_supa.goodstuff.message_log) {
+                this.data.addon.pv_supa.goodstuff.message_log = [];
+            }
+            if (this.data.addon.pv_supa.goodstuff.socket) {
+                    // don't know if we want to keep and re-use sockets
+                    // Previously I was test .socket.isAlive() and only
+                    // recreating if false
+                    dump('addon: pv_supa goodstuff destroying old socket\n');
+                    this.data.addon.pv_supa.goodstuff.socket.close();
+                    this.data.addon.pv_supa.goodstuff.socket = null;
+            }
+            if (! this.data.addon.pv_supa.goodstuff.socket) {
+                JSAN.use('util.socket');
+                this.data.addon.pv_supa.goodstuff.socket = new util.socket(
+
+                    this.prefs.prefHasUserValue('oils.addon.pvsupa.goodstuff.host')
+                    ? this.prefs.getCharPref('oils.addon.pvsupa.goodstuff.host')
+                    : '127.0.0.1',
+
+                    this.prefs.prefHasUserValue('oils.addon.pvsupa.goodstuff.port')
+                    ? this.prefs.getIntPref('oils.addon.pvsupa.goodstuff.port')
+                    : 5000 /* FIXME find out actual default port */
+                );
+                this.data.addon.pv_supa.goodstuff.socket.onStopRequest(
+                    function(request,context,result) {
+                        dump('addon: pv_supa goodstuff lost connection on page ' + location.href + '\n');
+                        obj.updateStatusBar('!lost connection\n');
+                        obj.data.addon.pv_supa.goodstuff.last_start_end_msg = null;
+                    }
+                );
+                dump('addon: pv_supa goodstuff socket opened\n');
+                this.updateStatusBar('!new connection\n');
+                obj.data.addon.pv_supa.goodstuff.last_start_end_msg = null;
+            } else {
+                dump('addon: pv_supa goodstuff socket already opened\n');
+            }
+            this.socket = this.data.addon.pv_supa.goodstuff.socket;
+            this.token = location.href + ' : ' + new Date();
+            this.data.addon.pv_supa.goodstuff.token = this.token;
+            var obj = this;
+            setTimeout(
+                function() {
+                    dump(
+                        'addon: pv_supa goodstuff socket\n\t' +
+                        'host: ' + obj.socket.host + '\n\t' +
+                        'port: ' + obj.socket.port + '\n\t' +
+                        'token: '+ obj.token
+                         + '\n'
+                    );
+                },
+                0
+            );
+        } catch(E) {
+            dump('addon: pv_supa Error in socket_init(): ' + E + '\n');
+        }
+    },
+    'updateStatusBar' : function(d) {
+        try {
+            var obj = this;
+            if (xulG && xulG.set_statusbar) {
+                if (obj.data.addon.pv_supa.goodstuff.message_log.length > 16) {
+                    obj.data.addon.pv_supa.goodstuff.message_log.shift();
+                }
+                obj.data.addon.pv_supa.goodstuff.message_log.push(d);
+                xulG.set_statusbar(
+                    5,
+                    'GoodStuff: ' + d,
+                    obj.data.addon.pv_supa.goodstuff.message_log.join(""),
+                    function(ev) {
+                        alert(
+                            obj.data.addon.pv_supa.goodstuff.message_log.join("")
+                        );
+                    }
+                );
+            }
+        } catch(E) {
+            dump('addon: pv_supa Error in updateStatusBar('+d+'): ' + E + '\n');
+        }
+    },
+    'onData' : function(f,security) {
+        dump('addon: setting pv_supa goodstuff onData callback, on page '
+            + location.href + ' with token ' + this.token + '\n');
+        var obj = this;
+        this.socket.dataCallback(
+            function(d) {
+                try {
+                    dump('addon: dataCallback func at page '
+                        + location.href + ' with token ' + obj.token + '\n');
+                    obj.updateStatusBar('>' + d);
+                    if (obj.token != obj.data.addon.pv_supa.goodstuff.token) {
+                        var e = 'addon error: pv supa: reading data out of turn\n';
+                        dump(e);
+                        obj.updateStatusBar('!' + e);
+                    }
+                    d= d.replace("\n","","g").replace("\r","","g").replace(" ","","g");
+                    var p = d.split("|");
+                    if (p.length == 1) {
+                        if (f) {
+                            f(p[0]); // hopefully a patron barcode
+                        }
+                    } else if (p[0] == 'START') {
+                        obj.data.addon.pv_supa.goodstuff.last_start_end_msg = p[0];
+                    } else if (p[0] == 'END') {
+                        obj.data.addon.pv_supa.goodstuff.last_start_end_msg = p[0];
+                    } else if (p[1] == 'NOK') {
+                        if (security) {
+                            var msg = $('addon_pvsupa_goodstuff_strings').getFormattedString(
+                                security == 'ACTIVATE'
+                                ? 'rfid.set_security_failure.prompt.message.activate_failure'
+                                : 'rfid.set_security_failure.prompt.message.deactivate_failure',
+                                [ p[0] ]
+                            );
+                            var choice = obj.error.yns_alert(
+                                msg,
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.set_security_failure.prompt.title'
+                                ),
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.set_security_failure.prompt.button.activate_security'
+                                ),
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.set_security_failure.prompt.button.deactivate_security'
+                                ),
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.set_security_failure.prompt.button.do_nothing_with_security'
+                                ),
+                                ''
+                            );
+                            if (choice == 0) {
+                                obj.write(p[0]+'|ACTIVATE\n');
+                            } else if (choice == 1) {
+                                obj.write(p[0]+'|DEACTIVATE\n');
+                            } else {
+                                obj.write(p[0]+'\n');
+                            }
+                        } else {
+                            dump('addon: unknown error\n');
+                        }
+                    } else if (p[1] == 'OK') {
+                        // ignore
+                    } else if (p[1].match('/')) {
+                        var counts = p[1].split('/');
+                        var read = counts[0];
+                        var set = counts[1];
+                        if (read != set) {
+                            var msg = $('addon_pvsupa_goodstuff_strings').getFormattedString(
+                                'rfid.partial_scan.prompt.message',
+                                [ read, set, p[0] ]
+                            );
+                            var choice = obj.error.yns_alert(
+                                msg,
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.partial_scan.prompt.title'
+                                ),
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.partial_scan.prompt.button.rescan'
+                                ),
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.partial_scan.prompt.button.skip'
+                                ),
+                                $('addon_pvsupa_goodstuff_strings').getString(
+                                    'rfid.partial_scan.prompt.button.proceed'
+                                ),
+                                ''
+                            );
+                            if (!choice || choice == 0) {
+                                obj.write(p[0]+'|REREAD\n');
+                            } else if (choice == 1) {
+                                obj.write(p[0]+'\n'); // do nothing, skip
+                            } else if (choice == 2) {
+                                if (f) {
+                                    f(p[0]); // hopefully an item barcode
+                                }
+                            }
+                        } else {
+                            if (f) {
+                                f(p[0]); // hopefully an item barcode
+                            }
+                        }
+                    } else {
+                        if (f) {
+                            f(p[0]); // no idea; shouldn't get here
+                        }
+                    }
+                } catch(E) {
+                    dump('addon: error in onData callback: ' + E + '\n');
+                }
+            }
+        );
+    },
+    'write' : function(s,ignore_token) {
+        dump('addon: write "' + s + '", on page ' + location.href + ' with token ' + this.token + '\n');
+        if ((this.token != this.data.addon.pv_supa.goodstuff.token) && !ignore_token) {
+            var e = 'addon error: pv supa: sending data out of turn\n';
+            dump(e);
+            this.updateStatusBar('!' + e);
+        }
+        if (!this.socket.socket.isAlive()) {
+            dump('addon error: pv supa: writing to not alive socket\n');
+        }
+        this.updateStatusBar('<' + s);
+        this.socket.write(s);
+    },
+    'request_items' : function() {
+        dump('addon: request_items on page ' + location.href + '\n');
+        if (this.data.addon.pv_supa.goodstuff.last_start_end_msg == 'START') {
+            this.write('END\n');
+        }
+        this.write('START|ITEM\n'); // we expect START|OK
+    },
+    'request_patrons' : function() {
+        dump('addon: request_patrons on page ' + location.href + '\n');
+        if (this.data.addon.pv_supa.goodstuff.last_start_end_msg == 'START') {
+            this.write('END\n');
+        }
+        this.write('START|PATRON\n'); // we expect START|OK
+    },
+    'end_session' : function() {
+        dump('addon: end_session on page ' + location.href + '\n');
+        if (this.token == this.data.addon.pv_supa.goodstuff.token) {
+            if (this.data.addon.pv_supa.goodstuff.last_start_end_msg == 'START') {
+                this.write('END\n');
+            }
+            this.data.addon.pv_supa.goodstuff.socket.close();
+            this.data.addon.pv_supa.goodstuff.socket = null;
+        }
+    },
+
+    'prefs_init' : function() {
+        dump('addon: pv_supa prefs_init\n');
+        var obj = this;
+
+        try {
+            if (! (g && g.addons_ui)) { return; }
+
+            const Cc = Components.classes;
+            const Ci = Components.interfaces;
+
+            function post_overlay() {
+                var tab = $('pv_supa_goodstuff_tab');
+                tab.setAttribute(
+                    'label',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.tab.label')
+                );
+                tab.setAttribute(
+                    'accesskey',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.tab.accesskey')
+                );
+
+                var enabled_label = $('pv_supa_goodstuff_enabled_label');
+                enabled_label.setAttribute(
+                    'value',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.checkbox.label')
+                );
+                enabled_label.setAttribute(
+                    'accesskey',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.checkbox.accesskey')
+                );
+
+                var host_label = $('pv_supa_goodstuff_hostname_label');
+                host_label.setAttribute(
+                    'value',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.host.label')
+                );
+                host_label.setAttribute(
+                    'accesskey',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.host.accesskey')
+                );
+
+                var port_label = $('pv_supa_goodstuff_port_label');
+                port_label.setAttribute(
+                    'value',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.port.label')
+                );
+                port_label.setAttribute(
+                    'accesskey',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.port.accesskey')
+                );
+
+                var save_btn = $('pv_supa_goodstuff_save_btn');
+                save_btn.setAttribute(
+                    'label',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.update.label')
+                );
+                save_btn.setAttribute(
+                    'accesskey',
+                    $('addon_pvsupa_goodstuff_strings').getString('prefs.update.accesskey')
+                );
+                save_btn.addEventListener(
+                    'command',
+                    function() {
+                        obj.save_prefs();
+                    },
+                    false
+                );
+                obj.display_prefs();
+            }
+
+            function myObserver() { this.register(); }
+            myObserver.prototype = {
+                register: function() {
+                    var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+                    observerService.addObserver(this, "xul-overlay-merged", false);
+                },
+                unregister: function() {
+                    var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+                    observerService.removeObserver(this, "xul-overlay-merged");
+                },
+                observe: function(subject,topic,data) {
+                    dump('observe: <'+subject+','+topic+','+data+'>\n');
+                    // setTimeout is needed here for xulrunner 1.8
+                    setTimeout( function() { try { post_overlay(); } catch(E) { alert(E); } }, 0 );
+                }
+            }
+
+            var observer = new myObserver();
+            var url = '/xul/server/addon/pv_supa_goodstuff_config_overlay.xul';
+            document.loadOverlay(location.protocol + '//' + location.hostname + url,observer)
+
+        } catch(E) {
+            dump('addon: pv_supa Error in prefs_init(): ' + E + '\n');
+        }
+    },
+
+    'display_prefs' : function() {
+        var obj = this;
+        try {
+            $('pv_supa_goodstuff_enabled_cb').checked = obj.active;
+            $('pv_supa_goodstuff_hostname_tb').value =
+                obj.prefs.prefHasUserValue('oils.addon.pvsupa.goodstuff.host')
+                ? obj.prefs.getCharPref('oils.addon.pvsupa.goodstuff.host')
+                : '127.0.0.1';
+
+            $('pv_supa_goodstuff_port_tb').value =
+                obj.prefs.prefHasUserValue('oils.addon.pvsupa.goodstuff.port')
+                ? obj.prefs.getIntPref('oils.addon.pvsupa.goodstuff.port')
+                : 5000; /* FIXME find out actual default port */
+
+        } catch(E) {
+            dump('addon: pv_supa Error in display_prefs(): ' + E + '\n');
+        }
+    },
+
+    'save_prefs' : function() {
+        var obj = this;
+        try {
+            obj.prefs.setBoolPref(
+                'oils.addon.pvsupa.goodstuff.enabled',
+                $('pv_supa_goodstuff_enabled_cb').checked
+            );
+            obj.prefs.setCharPref(
+                'oils.addon.pvsupa.goodstuff.host',
+                $('pv_supa_goodstuff_hostname_tb').value
+            );
+            obj.prefs.setIntPref(
+                'oils.addon.pvsupa.goodstuff.port',
+                $('pv_supa_goodstuff_port_tb').value
+            );
+            location.href = location.href;
+        } catch(E) {
+            dump('addon: pv_supa Error in save_prefs(): ' + E + '\n');
+        }
+    },
+
+    'checkin_init' : function() {
+        dump('addon: pv_supa checkin_init\n');
+        var obj = this;
+
+        try {
+
+            if (! (g && g.checkin)) { return; }
+
+            function setOnData() {
+                obj.onData(
+                    function(barcode) {
+                        g.checkin.controller.view.checkin_barcode_entry_textbox.value = barcode;
+                        g.checkin.checkin();
+                        // unlike checkout, I don't really care whether the checkin
+                        // succeeds or not; I think we should activate security on
+                        // the item and move on.  Errored items are still listed in
+                        // the interface and can be handled separately.
+                        obj.write(barcode+'|ACTIVATE\n');
+                    },
+                    'ACTIVATE'
+                );
+            }
+            setOnData();
+
+            var spacer = $('pcii3s');
+            var rfid_cb = document.createElement('checkbox');
+            rfid_cb.setAttribute('id','addon_rfid_cb');
+            rfid_cb.setAttribute(
+                'label',
+                $('addon_pvsupa_goodstuff_strings').getString(
+                    'rfid.checkbox.label'
+                )
+            );
+            rfid_cb.setAttribute(
+                'accesskey',
+                $('addon_pvsupa_goodstuff_strings').getString(
+                    'rfid.checkbox.accesskey'
+                )
+            );
+            spacer.parentNode.insertBefore(rfid_cb,spacer);
+            if (obj.prefs.prefHasUserValue(
+                    'oils.addon.pvsupa.goodstuff.checkin.rfid_checkbox.checked'
+            )){
+                rfid_cb.checked = obj.prefs.getBoolPref(
+                    'oils.addon.pvsupa.goodstuff.checkin.rfid_checkbox.checked'
+                );
+            }
+
+            if (rfid_cb.checked) {
+                obj.request_items();
+            }
+
+            rfid_cb.addEventListener(
+                'command',
+                function(ev) {
+                    if (ev.target.checked) {
+                        obj.socket_init();
+                        setTimeout(
+                            function() {
+                                setOnData();
+                                obj.request_items();
+                            }, 1000
+                        );
+                    } else {
+                        obj.end_session();
+                    }
+                    obj.prefs.setBoolPref(
+                        'oils.addon.pvsupa.goodstuff.checkin.rfid_checkbox.checked',
+                        ev.target.checked
+                    );
+                }
+            );
+
+            window.addEventListener(
+                'unload',
+                function(ev) {
+                    obj.end_session();
+                },
+                false
+            );
+            window.addEventListener(
+                'tab_focus',
+                function(ev) {
+                    obj.socket_init();
+                    if (rfid_cb.checked) {
+                        setTimeout(
+                            function() {
+                                setOnData();
+                                obj.request_items();
+                            }, 1000
+                        );
+                    }
+                },
+                false
+            );
+
+        } catch(E) {
+            dump('addon: pv_supa Error in checkin_init(): ' + E + '\n');
+        }
+    },
+    'checkout_init' : function() {
+        dump('addon: pv_supa checkout_init\n');
+        var obj = this;
+
+        try {
+
+            if (! (g && g.checkout)) { return; }
+
+            function setOnData() {
+                obj.onData(
+                    function(barcode) {
+                        g.checkout.controller.view.checkout_barcode_entry_textbox.value = barcode;
+                        var pre_list_count = g.checkout.list.row_count.total;
+                        g.checkout.checkout({'barcode':barcode});
+                        var post_list_count = g.checkout.list.row_count.total;
+                        if (pre_list_count != post_list_count) {
+                            obj.write(barcode+'|DEACTIVATE\n'); // checkout success
+                        } else {
+                            obj.write(barcode+'|ACTIVATE\n'); // checkout failure
+                        }
+                    },
+                    'DEACTIVATE'
+                );
+            }
+            setOnData();
+
+            var spacer = $('pcii3s');
+            var rfid_cb = document.createElement('checkbox');
+            rfid_cb.setAttribute('id','addon_rfid_cb');
+            rfid_cb.setAttribute(
+                'label',
+                $('addon_pvsupa_goodstuff_strings').getString(
+                    'rfid.checkbox.label'
+                )
+            );
+            rfid_cb.setAttribute(
+                'accesskey',
+                $('addon_pvsupa_goodstuff_strings').getString(
+                    'rfid.checkbox.accesskey'
+                )
+            );
+            spacer.parentNode.insertBefore(rfid_cb,spacer);
+            if (obj.prefs.prefHasUserValue(
+                    'oils.addon.pvsupa.goodstuff.checkout.rfid_checkbox.checked'
+            )){
+                rfid_cb.checked = obj.prefs.getBoolPref(
+                    'oils.addon.pvsupa.goodstuff.checkout.rfid_checkbox.checked'
+                );
+            }
+
+            if (rfid_cb.checked) {
+                obj.request_items();
+            }
+
+            rfid_cb.addEventListener(
+                'command',
+                function(ev) {
+                    if (ev.target.checked) {
+                        obj.socket_init();
+                        setTimeout(
+                            function() {
+                                setOnData();
+                                obj.request_items();
+                            }, 1000
+                        );
+                    } else {
+                        obj.end_session();
+                    }
+                    obj.prefs.setBoolPref(
+                        'oils.addon.pvsupa.goodstuff.checkout.rfid_checkbox.checked',
+                        ev.target.checked
+                    );
+                }
+            );
+
+            window.addEventListener(
+                'unload',
+                function(ev) {
+                    obj.end_session();
+                },
+                false
+            );
+            window.addEventListener(
+                'tab_focus',
+                function(ev) {
+                    obj.socket_init();
+                    if (rfid_cb.checked) {
+                        setTimeout(
+                            function() {
+                                setOnData();
+                                obj.request_items();
+                            }, 1000
+                        );
+                    }
+                },
+                false
+            );
+
+        } catch(E) {
+            dump('addon: pv_supa Error in checkout_init(): ' + E + '\n');
+        }
+    },
+    'patron_for_checkout_init' : function() {
+        dump('addon: pv_supa patron_for_checkout_init\n');
+        var obj = this;
+
+        try {
+
+            if (! String(location.href).match('/patron/barcode_entry.xul')) { return; }
+
+            function setOnData() {
+                obj.onData(
+                    function(barcode) {
+                        obj.write(barcode+'\n');
+                        $('barcode_tb').value = barcode;
+                        window.submit();
+                    },
+                    'DEACTIVATE'
+                );
+            }
+            setOnData();
+
+            var hbox = $('barcode_tb').parentNode;
+            var rfid_cb = document.createElement('checkbox');
+            rfid_cb.setAttribute('id','addon_rfid_cb');
+            rfid_cb.setAttribute(
+                'label',
+                $('addon_pvsupa_goodstuff_strings').getString(
+                    'rfid.checkbox.label'
+                )
+            );
+            rfid_cb.setAttribute(
+                'accesskey',
+                $('addon_pvsupa_goodstuff_strings').getString(
+                    'rfid.checkbox.accesskey'
+                )
+            );
+            hbox.appendChild(rfid_cb);
+            if (obj.prefs.prefHasUserValue(
+                    'oils.addon.pvsupa.goodstuff.patron_for_checkout.rfid_checkbox.checked'
+            )){
+                rfid_cb.checked = obj.prefs.getBoolPref(
+                    'oils.addon.pvsupa.goodstuff.patron_for_checkout.rfid_checkbox.checked'
+                );
+            }
+
+            if (rfid_cb.checked) {
+                obj.request_patrons();
+            }
+
+            rfid_cb.addEventListener(
+                'command',
+                function(ev) {
+                    if (ev.target.checked) {
+                        obj.socket_init();
+                        setTimeout(
+                            function() {
+                                setOnData();
+                                obj.request_patrons();
+                            }, 1000
+                        );
+                    } else {
+                        obj.end_session();
+                    }
+                    obj.prefs.setBoolPref(
+                        'oils.addon.pvsupa.goodstuff.patron_for_checkout.rfid_checkbox.checked',
+                        ev.target.checked
+                    );
+                }
+            );
+
+            window.addEventListener(
+                'unload',
+                function(ev) {
+                    obj.end_session();
+                },
+                false
+            );
+            window.addEventListener(
+                'tab_focus',
+                function(ev) {
+                    obj.socket_init();
+                    if (rfid_cb.checked) {
+                        setTimeout(
+                            function() {
+                                setOnData();
+                                obj.request_patrons();
+                            }, 1000
+                        );
+                    }
+                },
+                false
+            );
+
+        } catch(E) {
+            dump('addon: pv_supa Error in patron_for_checkout_init(): ' + E + '\n');
+        }
+    }
+
+
+
+}
+
diff --git a/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_config_overlay.xul b/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_config_overlay.xul
new file mode 100644 (file)
index 0000000..f57e4a6
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+<overlay id="pv_supa_goodstuff_config_overlay"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script>dump('loading addon/pv_supa_goodstuff_config_overlay.xul\n');</script>
+
+<tabs id="addonpref_tabs">
+    <tab id="pv_supa_goodstuff_tab" />
+</tabs>
+
+<tabpanels id="addonpref_tabpanels">
+    <tabpanel id="pv_supa_goodstuff_tabpanel">
+        <grid>
+            <columns>
+                <column/>
+                <column/>
+            </columns>
+            <rows>
+                <row>
+                    <label id="pv_supa_goodstuff_enabled_label"
+                        control="pv_supa_goodstuff_enabled_cb"/>
+                    <checkbox id="pv_supa_goodstuff_enabled_cb"/>
+                </row>
+                <row>
+                    <label id="pv_supa_goodstuff_hostname_label"
+                        control="pv_supa_goodstuff_hostname_tb"/>
+                    <textbox id="pv_supa_goodstuff_hostname_tb"/>
+                </row>
+                <row>
+                    <label id="pv_supa_goodstuff_port_label"
+                        control="pv_supa_goodstuff_port_tb"/>
+                    <textbox id="pv_supa_goodstuff_port_tb"/>
+                </row>
+                <row>
+                    <spacer/>
+                    <button id="pv_supa_goodstuff_save_btn" />
+                </row>
+            </rows>
+        </grid>
+    </tabpanel>
+</tabpanels>
+
+</overlay>
diff --git a/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test1.expect b/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test1.expect
new file mode 100755 (executable)
index 0000000..edfeb60
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/expect -f
+# This test simulates interaction with GoodStuff hardware for the purpose
+# of RFID scanning a patron card.  The hardware is capable of fielding
+# multiple such cards in one pass, but the staff client will close the
+# connection after the first successful card.
+#
+# Prerequisites:
+#
+# Requires expect and netcat to be installed, and expects for a pristine
+# load of EG's stock test data to be installed.  In the staff client,
+# pv_supa_goodstuff should be listed in the Add-Ons list under Admin ->
+# Workstation Administration -> Server Add-ons.  In the GoodStuff
+# preferences section, Enabled should be checked, the IP/Hostname field
+# should point to the server running this test script, and the port
+# should be 5000.  Networking should be configured to allow the client
+# machine to reach port 5000 on this server.  If netcat is not installed
+# as /bin/nc, change the spawn line below as appropriate.
+#
+# Steps:
+#
+# 1) Ensure the staff client is configured as per the prerequisites, and
+#    clear all tabs in the staff client.
+# 2) Invoke this script.
+# 3) In the staff client, press F1 or invoke Circulation -> Checkout
+#    Items.  If the RFID checkbox is unchecked, check it.
+# 4) The script should run without errors, and the staff client should
+#    show an attempt at loading a patron with card "bad_card", and then
+#    the patron with card "99999376669" should load.  The script should
+#    end without errors.
+#
+set send_slow {1 .1}
+proc send {ignore arg} {
+    sleep .1
+    exp_send -s -- $arg
+}
+set timeout -1
+spawn "/bin/nc/" -l -p 5000
+match_max 100000
+expect "START|PATRON\r"
+send -- "START|OK\r"
+expect -exact "START|OK\r
+"
+send -- "bad_card\r"
+expect -exact "bad_card\r
+bad_card\r
+"
+send -- "99999376669\r"
+expect -exact "99999376669\r
+99999376669\r
+END\r
+"
+expect eof
diff --git a/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test2.expect b/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test2.expect
new file mode 100755 (executable)
index 0000000..4115c14
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/expect -f
+# This test simulates interaction with GoodStuff hardware for the purpose
+# of RFID scanning items for checkout.
+#
+# Prerequisites:
+#
+# Requires expect and netcat to be installed, and expects for a pristine
+# load of EG's stock test data to be installed.  In the staff client,
+# pv_supa_goodstuff should be listed in the Add-Ons list under Admin ->
+# Workstation Administration -> Server Add-ons.  In the GoodStuff
+# preferences section, Enabled should be checked, the IP/Hostname field
+# should point to the server running this test script, and the port
+# should be 5000.  Networking should be configured to allow the client
+# machine to reach port 5000 on this server.  If netcat is not installed
+# as /bin/nc, change the spawn line below as appropriate.
+#
+# Steps:
+#
+# 1) Ensure the staff client is configured as per the prerequisites, and
+#    clear all tabs in the staff client.
+# 2) Invoke this script.
+# 3) In the staff client, press F4 or invoke Search -> Search for
+#    Patrons.  Enter Smith for the last name and submit the search.
+#    Retrieve the first patron, Cathy Smith.  If the RFID checkbox is
+#    unchecked, check it.
+# 4) The client will attempt to circulate CONC4000049, which is already
+#    checked out to another user.  Click "Cancel".
+# 5) The client will attempt to circulate CONC4000048, which is already
+#    checked out to another user.  Click "Normal Checkin then Checkout".
+# 6) The client will circulate CONC70000408, but the fake GoodStuff will
+#    fail to deactivate the security flag for the item.  Click
+#    "Deactivate Security".
+# 7) The client will circulate CONC70000401, but the fake GoodStuff will
+#    fail to deactivate the security flag for the item.  Click "Make No
+#    Change".
+# 8) The fake GoodStuff will report a problem with CONC70000394, that it
+#    expected two parts but could only detect one.  Click "Re-Scan Item".
+#    This item should circulate.
+# 9) The fake GoodStuff will report a problem with CONC70000387, that it
+#    expected two parts but could only detect one.  Click "Proceed with
+#    Item".  This item should circulate.
+# 10) Close the tab or click Done (and cancel any print job).  The script
+#     should end without errors.
+#
+# Note: added some gratuitous whitespace near the end with the socket
+#       messages; client should ignore whitespace
+#
+set send_slow {1 .1}
+proc send {ignore arg} {
+    sleep .1
+    exp_send -s -- $arg
+}
+set timeout -1
+spawn "/bin/nc" -l -p 5000
+match_max 100000
+expect "START|ITEM\r"
+send -- "START|OK\r"
+expect -exact "START|OK\r
+"
+send -- "CONC4000049"
+expect -exact "CONC4000049"
+send -- "|1/1\r"
+expect -exact "|1/1\r
+CONC4000049|ACTIVATE\r
+"
+send -- "CONC4000049"
+expect -exact "CONC4000049"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "CONC4000048"
+expect -exact "CONC4000048"
+send -- "|1/1\r"
+expect -exact "|1/1\r
+CONC4000048|DEACTIVATE\r
+"
+send -- "CONC4000048"
+expect -exact "CONC4000048"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "CONC70000408"
+expect -exact "CONC70000408"
+send -- "|1/1\r"
+expect -exact "|1/1\r
+CONC70000408|DEACTIVATE\r
+"
+send -- "CONC70000408"
+expect -exact "CONC70000408"
+send -- "|NOK\r"
+expect -exact "|NOK\r
+CONC70000408|DEACTIVATE\r
+"
+send -- "CONC70000408"
+expect -exact "CONC70000408"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "CONC70000401"
+expect -exact "CONC70000401"
+send -- "|1/1\r"
+expect -exact "|1/1\r
+CONC70000401|DEACTIVATE\r
+"
+send -- "CONC70000401"
+expect -exact "CONC70000401"
+send -- "|NOK\r"
+expect -exact "|NOK\r
+CONC70000401\r
+"
+send -- "CONC70000401"
+expect -exact "CONC70000401"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "CONC70000394"
+expect -exact "CONC70000394"
+send -- "|1/2\r"
+expect -exact "|1/2\r
+CONC70000394|REREAD\r
+"
+send -- "CONC70000394"
+expect -exact "CONC70000394"
+send -- "|2/2\r"
+expect -exact "|2/2\r
+CONC70000394|DEACTIVATE\r
+"
+send -- "CONC70000394"
+expect -exact "CONC70000394"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "CONC70000387 "
+expect -exact "CONC70000387 "
+send -- "| 1/ 2 \r"
+expect -exact "| 1/ 2 \r
+CONC70000387|DEACTIVATE\r
+"
+send -- "CONC70000387"
+expect -exact "CONC70000387"
+send -- "|OK\r"
+expect -exact "|OK\r
+END\r"
+expect eof
diff --git a/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test3.expect b/Open-ILS/xul/staff_client/server/addon/pv_supa_goodstuff_tests/pv_supa_goodstuff_test3.expect
new file mode 100755 (executable)
index 0000000..a342034
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/expect -f
+# This test simulates interaction with GoodStuff hardware for the purpose
+# of RFID scanning items for checkout.
+#
+# Prerequisites:
+#
+# Requires expect and netcat to be installed, and expects for a pristine
+# load of EG's stock test data to be installed.  In the staff client,
+# pv_supa_goodstuff should be listed in the Add-Ons list under Admin ->
+# Workstation Administration -> Server Add-ons.  In the GoodStuff
+# preferences section, Enabled should be checked, the IP/Hostname field
+# should point to the server running this test script, and the port
+# should be 5000.  Networking should be configured to allow the client
+# machine to reach port 5000 on this server.  If netcat is not installed
+# as /bin/nc, change the spawn line below as appropriate.  The staff
+# client should registered to the BR1 branch and the stock admin user
+# should be used.
+#
+# Steps:
+#
+# 1) Ensure the staff client is configured as per the prerequisites, and
+#    clear all tabs in the staff client.
+# 2) Invoke this script.
+# 3) In the staff client, press F2 or invoke Circulation -> Check In Items.
+#    If the RFID checkbox is unchecked, check it.
+# 4) The client will transit item CONC90000480.  Click "Do Not Print".
+# 5) The client will fail to check in item "madeupitem".  Click "OK".
+# 6) The fake GoodStuff will report a problem scanning all the parts for
+#    item CONC40000120, and say that it read only 1 of 2 parts.  Click
+#    "Re-Scan Item".  The item will be checked in.
+# 7) The client will check-in item CONC40000121, but the fake GoodStuff
+#    will report a problem setting the item's security flag.  Click
+#    "Activate Security".  The problem will persist.  Click "Make No
+#    Change".
+# 8) Close the tab.  The script should end without errors.  This
+#    particular test may be ran again with identical results without
+#    needing to reset the test data.
+#
+set send_slow {1 .1}
+proc send {ignore arg} {
+    sleep .1
+    exp_send -s -- $arg
+}
+set timeout -1
+spawn "/bin/nc" -l -p 5000
+match_max 100000
+expect "START|ITEM\r"
+send -- "START|OK\r"
+expect -exact "START|OK\r
+"
+send -- "CONC90000480"
+expect -exact "CONC90000480"
+send -- "|1/1\r"
+expect -exact "|1/1\r
+CONC90000480|ACTIVATE\r
+"
+send -- "CONC90000480"
+expect -exact "CONC90000480"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "madeupitem|1/1\r"
+expect -exact "madeupitem|1/1\r
+madeupitem|ACTIVATE\r
+"
+send -- "madeupitem|OK\r"
+expect -exact "madeupitem|OK\r
+"
+send -- "CONC40000120"
+expect -exact "CONC40000120"
+send -- "|1/2\r"
+expect -exact "|1/2\r
+CONC40000120|REREAD\r
+"
+send -- "CONC40000120"
+expect -exact "CONC40000120"
+send -- "|2/2\r"
+expect -exact "|2/2\r
+CONC40000120|ACTIVATE\r
+"
+send -- "CONC40000120"
+expect -exact "CONC40000120"
+send -- "|OK\r"
+expect -exact "|OK\r
+"
+send -- "CONC40000121"
+expect -exact "CONC40000121"
+send -- "|1/1\r"
+expect -exact "|1/1\r
+CONC40000121|ACTIVATE\r
+"
+send -- "CONC40000121"
+expect -exact "CONC40000121"
+send -- "|NOK\r"
+expect -exact "|NOK\r
+CONC40000121|ACTIVATE\r
+"
+send -- "CONC40000121"
+expect -exact "CONC40000121"
+send -- "|NOK\r"
+expect -exact "|NOK\r
+CONC40000121\r
+"
+send -- "CONC40000121"
+expect -exact "CONC40000121"
+send -- "|OK\r"
+expect -exact "|OK\r
+END\r"
+expect eof
diff --git a/Open-ILS/xul/staff_client/server/locale/en-US/addon/pv_supa_goodstuff.properties b/Open-ILS/xul/staff_client/server/locale/en-US/addon/pv_supa_goodstuff.properties
new file mode 100644 (file)
index 0000000..6ef59e9
--- /dev/null
@@ -0,0 +1,23 @@
+rfid.checkbox.label=RFID
+rfid.checkbox.accesskey=
+rfid.partial_scan.prompt.title=RFID Partial Scan
+rfid.partial_scan.prompt.message=Read %1$s of %2$s parts for item %3$s.
+rfid.partial_scan.prompt.button.rescan=Re-Scan Item
+rfid.partial_scan.prompt.button.skip=Skip to Next Item
+rfid.partial_scan.prompt.button.proceed=Proceed with Item
+rfid.set_security_failure.prompt.title=RFID Security Error
+rfid.set_security_failure.prompt.message.activate_failure=Failed to activate the security flag on item %1$s.
+rfid.set_security_failure.prompt.message.deactivate_failure=Failed to deactivate the security flag on item %1$s.
+rfid.set_security_failure.prompt.button.activate_security=Activate Security
+rfid.set_security_failure.prompt.button.deactivate_security=Deactivate Security
+rfid.set_security_failure.prompt.button.do_nothing_with_security=Make No Change
+prefs.tab.label=GoodStuff
+prefs.tab.accesskey=
+prefs.checkbox.label=Enabled
+prefs.checkbox.accesskey=
+prefs.host.label=IP/Hostname
+prefs.host.accesskey=
+prefs.port.label=Port
+prefs.port.accesskey=
+prefs.update.label=Update
+prefs.update.accesskey=
diff --git a/Open-ILS/xul/staff_client/server/skin/addon/pv_supa_goodstuff.css b/Open-ILS/xul/staff_client/server/skin/addon/pv_supa_goodstuff.css
new file mode 100644 (file)
index 0000000..d588001
--- /dev/null
@@ -0,0 +1 @@
+@import url("pv_supa_goodstuff_custom.css");
diff --git a/docs/RELEASE_NOTES_NEXT/pv_supa_goodstuff.txt b/docs/RELEASE_NOTES_NEXT/pv_supa_goodstuff.txt
new file mode 100644 (file)
index 0000000..124da33
--- /dev/null
@@ -0,0 +1,26 @@
+Upgrade Notes
+-------------
+P.V. SUPA GoodStuff Integration
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+There is now a "Server Add-ons" module for integrating P.V. Supa's RFID product
+known as GoodStuff with the Evergreen staff client.
+
+To activate it, you should add the identifier "pv_supa_goodstaff" (without the
+quotes) to the list managed by the Admin->Workstation Administration->Server
+Add-ons menu action within the staff client.  You will need the
+ADMIN_SERVER_ADDON_FOR_WORKSTATION permission to do this.
+
+After doing this and clicking the Update Active Add-Ons button, the interface
+will refresh and show a GoodStuff tab in the Add-on Preferences section.  Within
+this tab you will have the option of specifying the hostname and port for the
+Goodstaff hardware. There is also an "Enabled" setting that needs to be checked.
+
+Currently three interfaces have been integrated:
+* Circulation -> Check In Items
+* Circulation -> Check Out Items (where you scan the patron barcode)
+* Circulation -> Check Out Items (where you scan the item barcodes)
+
+Each interface gets an RFID checkbox if the "Enabled" preference has been set,
+that can activate/deactivate the functionality on a per interface basis.  The
+checkbox states persist (i.e. are sticky).
+