Bugzilla – Attachment 19065 Details for
Bug 10240
Offline circulation using HTML5 and IndexedDB
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 10240: Offline circulation using HTML5 and IndexedDB
Bug-10240-Offline-circulation-using-HTML5-and-Inde.patch (text/plain), 69.97 KB, created by
Jared Camins-Esakov
on 2013-06-17 11:30:05 UTC
(
hide
)
Description:
Bug 10240: Offline circulation using HTML5 and IndexedDB
Filename:
MIME Type:
Creator:
Jared Camins-Esakov
Created:
2013-06-17 11:30:05 UTC
Size:
69.97 KB
patch
obsolete
>From 298b6bb82ee3537ec3ad378a8eabbf408e8fbe8f Mon Sep 17 00:00:00 2001 >From: Jared Camins-Esakov <jcamins@cpbibliography.com> >Date: Sat, 4 May 2013 18:48:54 -0400 >Subject: [PATCH] Bug 10240: Offline circulation using HTML5 and IndexedDB > >This patch adds an HTML5-based offline mode to Koha's existing >circulation module, allowing librarians to check out items using a >basically familiar interface. The feature will be implemented using >the Application Cache and IndexedDB features of the HTML5 specification, >both of which are fully supported on Firefox 10+ and Chrome 23+, with >limited support going back to Firefox 4 and Chrome 11. The basic >workflow enabled by this patch is as follows: > >Part 1: While connected to the Internet >1. Enable offline functionality by turning on the > "AllowOfflineCirculation" system preference. >2. Sync the offline circulation database on the computer that will be > used for offline circulation by following the "Offline circulation > interface" link on the Circulation home page, choosing "Synchronize (must be online)", > and clicking the "Download records" button. This process may take a while. >3. Bookmark /cgi-bin/koha/circ/offline.pl (the page you are currently > on) for easy access when offline. > >Part 2: While disconnected from the Internet >4. Navigate to /cgi-bin/koha/circ/offline.pl using the bookmark you > created while online. >5. Start checking books in by scanning the barcode of an item that has > been returned into the box in the "Check in" tab. >6. Scan the barcodes of any additional items that have been returned. >7. Start checking out books to a patron by scanning the patron's barcode > in the box in the "Check out" tab. >8. Set a due date (the "Remember for session" box will be checked by > default, since circulation rules are not computed during offline > transactions and therefore a due date must be specified by the > librarian). >9. Scan an item barcode (if you did not set a due date, it will prompt > you) to check the item out to the patron. >10. If a patron has a fine you can see the total amount (current to when > the offline module was synced), and record a payment. Unlike when in > online mode, there will be no breakdown of what item(s) fines are > for, and you will only be able to record the payment amount and not > associate it with a particular item. > >Part 3: While connected to the Internet >11. Click the "Synchronize" link and choose "Upload transactions" to > upload the transactions recorded during the offline circulation > session. >12. Navigate to /cgi-bin/koha/offline_circ/list.pl (there will be a > link from the Offline circulation page) and review the > transactions, as described in the documentation for the Firefox > Offline circulation plugin: > http://wiki.koha-community.org/wiki/Offline_circulation_firefox_plugin >--- > circ/circulation-home.pl | 1 + > circ/offline-mf.pl | 34 + > circ/offline.pl | 36 + > installer/data/mysql/sysprefs.sql | 1 + > installer/data/mysql/updatedatabase.pl | 7 + > .../lib/jquery/plugins/jquery.indexeddb.js | 517 +++++++++++++++ > .../intranet-tmpl/prog/en/css/staff-global.css | 26 +- > koha-tmpl/intranet-tmpl/prog/en/js/offlinecirc.js | 109 ++++ > .../en/modules/admin/preferences/circulation.pref | 6 + > .../prog/en/modules/circ/circulation-home.tt | 15 +- > .../prog/en/modules/circ/offline-mf.tt | 43 ++ > .../intranet-tmpl/prog/en/modules/circ/offline.tt | 690 ++++++++++++++++++++ > offline_circ/download.pl | 107 +++ > offline_circ/service.pl | 1 + > 14 files changed, 1586 insertions(+), 7 deletions(-) > create mode 100755 circ/offline-mf.pl > create mode 100755 circ/offline.pl > create mode 100644 koha-tmpl/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/js/offlinecirc.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline-mf.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline.tt > create mode 100755 offline_circ/download.pl > >diff --git a/circ/circulation-home.pl b/circ/circulation-home.pl >index d4f5a2c..d10829f 100755 >--- a/circ/circulation-home.pl >+++ b/circ/circulation-home.pl >@@ -39,6 +39,7 @@ $template->param( fast_cataloging => 1 ) if (defined $fa); > > # Checking if the transfer page needs to be displayed > $template->param( display_transfer => 1 ) if ( ($flags->{'superlibrarian'} == 1) || (C4::Context->preference("IndependentBranches") == 0) ); >+$template->{'VARS'}->{'AllowOfflineCirculation'} = C4::Context->preference('AllowOfflineCirculation'); > > > output_html_with_http_headers $query, $cookie, $template->output; >diff --git a/circ/offline-mf.pl b/circ/offline-mf.pl >new file mode 100755 >index 0000000..010c86a >--- /dev/null >+++ b/circ/offline-mf.pl >@@ -0,0 +1,34 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along with >+# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place, >+# Suite 330, Boston, MA 02111-1307 USA >+# >+ >+use Modern::Perl; >+use CGI; >+use C4::Auth; >+ >+my $query = new CGI; >+my ($template, $loggedinuser, $cookie, $flags) >+= get_template_and_user({template_name => "circ/offline-mf.tt", >+ query => $query, >+ type => "intranet", >+ authnotrequired => 0, >+ flagsrequired => {circulate => "circulate_remaining_permissions"}, >+ }); >+ >+$template->{'VARS'}->{'cookie'} = $cookie; >+print $query->header(-type => 'text/cache-manifest', cookie => $cookie); >+print $template->output; >diff --git a/circ/offline.pl b/circ/offline.pl >new file mode 100755 >index 0000000..b33dc5e >--- /dev/null >+++ b/circ/offline.pl >@@ -0,0 +1,36 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along with >+# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place, >+# Suite 330, Boston, MA 02111-1307 USA >+# >+ >+use Modern::Perl; >+use CGI; >+use C4::Auth; >+use C4::Output; >+use C4::Context; >+ >+my $query = new CGI; >+my ($template, $loggedinuser, $cookie, $flags) >+= get_template_and_user({template_name => "circ/offline.tt", >+ query => $query, >+ type => "intranet", >+ authnotrequired => 0, >+ flagsrequired => {circulate => "circulate_remaining_permissions"}, >+ }); >+ >+$template->{'VARS'}->{'AllowOfflineCirculation'} = C4::Context->preference('AllowOfflineCirculation'); >+$template->{'VARS'}->{'maxoutstanding'} = C4::Context->preference('maxoutstanding') || 0; >+output_html_with_http_headers $query, $cookie, $template->output; >diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql >index fbd0387..c15e33b 100644 >--- a/installer/data/mysql/sysprefs.sql >+++ b/installer/data/mysql/sysprefs.sql >@@ -427,3 +427,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES(' > INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('UseCourseReserves', '0', 'Enable the course reserves feature.', NULL, 'YesNo'); > INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacHoldNotes',0,'Show hold notes on OPAC','','YesNo'); > INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ('CalculateFinesOnReturn','1','Switch to control if overdue fines are calculated on return or not', '', 'YesNo'); >+INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('AllowOfflineCirculation','0','','If on, enables HTML5 offline circulation functionality.','YesNo'); >diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl >index 5963d24..77a6c00 100755 >--- a/installer/data/mysql/updatedatabase.pl >+++ b/installer/data/mysql/updatedatabase.pl >@@ -7010,6 +7010,13 @@ CREATE TABLE IF NOT EXISTS borrower_files ( > SetVersion($DBversion); > } > >+$DBversion = "3.13.00.XXX"; >+if ( CheckVersion($DBversion) ) { >+ $dbh->do("INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('AllowOfflineCirculation','0','','If on, enables HTML5 offline circulation functionality.','YesNo')"); >+ print "Upgrade to $DBversion done (Bug 10240: Add syspref AllowOfflineCirculation)\n"; >+ SetVersion ($DBversion); >+} >+ > =head1 FUNCTIONS > > =head2 TableExists($table) >diff --git a/koha-tmpl/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js b/koha-tmpl/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js >new file mode 100644 >index 0000000..e75a2a5 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js >@@ -0,0 +1,517 @@ >+(function($, undefined) { >+ var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; >+ var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange; >+ var IDBCursor = window.IDBCursor || window.webkitIDBCursor; >+ IDBCursor.PREV = IDBCursor.PREV || "prev"; >+ IDBCursor.NEXT = IDBCursor.NEXT || "next"; >+ >+ /** >+ * Best to use the constant IDBTransaction since older version support numeric types while the latest spec >+ * supports strings >+ */ >+ var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction; >+ >+ function getDefaultTransaction(mode) { >+ var result = null; >+ switch (mode) { >+ case 0: >+ case 1: >+ case "readwrite": >+ case "readonly": >+ result = mode; >+ break; >+ default: >+ result = IDBTransaction.READ_WRITE || "readwrite"; >+ } >+ return result; >+ } >+ >+ $.extend({ >+ /** >+ * The IndexedDB object used to open databases >+ * @param {Object} dbName - name of the database >+ * @param {Object} config - version, onupgradeneeded, onversionchange, schema >+ */ >+ "indexedDB": function(dbName, config) { >+ if (config) { >+ // Parse the config argument >+ if (typeof config === "number") config = { >+ "version": config >+ }; >+ >+ var version = config.version; >+ if (config.schema && !version) { >+ var max = -1; >+ for (key in config.schema) { >+ max = max > key ? max : key; >+ } >+ version = config.version || max; >+ } >+ } >+ >+ >+ var wrap = { >+ "request": function(req, args) { >+ return $.Deferred(function(dfd) { >+ try { >+ var idbRequest = typeof req === "function" ? req(args) : req; >+ idbRequest.onsuccess = function(e) { >+ >+ dfd.resolveWith(idbRequest, [idbRequest.result, e]); >+ }; >+ idbRequest.onerror = function(e) { >+ >+ dfd.rejectWith(idbRequest, [idbRequest.error, e]); >+ }; >+ if (typeof idbRequest.onblocked !== "undefined" && idbRequest.onblocked === null) { >+ idbRequest.onblocked = function(e) { >+ >+ var res; >+ try { >+ res = idbRequest.result; >+ } catch (e) { >+ res = null; // Required for Older Chrome versions, accessing result causes error >+ } >+ dfd.notifyWith(idbRequest, [res, e]); >+ }; >+ } >+ if (typeof idbRequest.onupgradeneeded !== "undefined" && idbRequest.onupgradeneeded === null) { >+ idbRequest.onupgradeneeded = function(e) { >+ >+ dfd.notifyWith(idbRequest, [idbRequest.result, e]); >+ }; >+ } >+ } catch (e) { >+ e.name = "exception"; >+ dfd.rejectWith(idbRequest, ["exception", e]); >+ } >+ }); >+ }, >+ // Wraps the IDBTransaction to return promises, and other dependent methods >+ "transaction": function(idbTransaction) { >+ return { >+ "objectStore": function(storeName) { >+ try { >+ return wrap.objectStore(idbTransaction.objectStore(storeName)); >+ } catch (e) { >+ idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort(); >+ return wrap.objectStore(null); >+ } >+ }, >+ "createObjectStore": function(storeName, storeParams) { >+ try { >+ return wrap.objectStore(idbTransaction.db.createObjectStore(storeName, storeParams)); >+ } catch (e) { >+ idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort(); >+ } >+ }, >+ "deleteObjectStore": function(storeName) { >+ try { >+ idbTransaction.db.deleteObjectStore(storeName); >+ } catch (e) { >+ idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort(); >+ } >+ }, >+ "abort": function() { >+ idbTransaction.abort(); >+ } >+ }; >+ }, >+ "objectStore": function(idbObjectStore) { >+ var result = {}; >+ // Define CRUD operations >+ var crudOps = ["add", "put", "get", "delete", "clear", "count"]; >+ for (var i = 0; i < crudOps.length; i++) { >+ result[crudOps[i]] = (function(op) { >+ return function() { >+ return wrap.request(function(args) { >+ return idbObjectStore[op].apply(idbObjectStore, args); >+ }, arguments); >+ }; >+ })(crudOps[i]); >+ } >+ >+ result.each = function(callback, range, direction) { >+ return wrap.cursor(function() { >+ if (direction) { >+ return idbObjectStore.openCursor(wrap.range(range), direction); >+ } else { >+ return idbObjectStore.openCursor(wrap.range(range)); >+ } >+ }, callback); >+ }; >+ >+ result.index = function(name) { >+ return wrap.index(function() { >+ return idbObjectStore.index(name); >+ }); >+ }; >+ >+ result.createIndex = function(prop, options, indexName) { >+ if (arguments.length === 2 && typeof options === "string") { >+ indexName = arguments[1]; >+ options = null; >+ } >+ if (!indexName) { >+ indexName = prop; >+ } >+ return wrap.index(function() { >+ return idbObjectStore.createIndex(indexName, prop, options); >+ }); >+ }; >+ >+ result.deleteIndex = function(indexName) { >+ return idbObjectStore.deleteIndex(indexName); >+ }; >+ >+ return result; >+ }, >+ >+ "range": function(r) { >+ if ($.isArray(r)) { >+ if (r.length === 1) { >+ return IDBKeyRange.only(r[0]); >+ } else { >+ return IDBKeyRange.bound(r[0], r[1], (typeof r[2] === 'undefined') ? true : r[2], (typeof r[3] === 'undefined') ? true : r[3]); >+ } >+ } else if (typeof r === "undefined") { >+ return null; >+ } else { >+ return r; >+ } >+ }, >+ >+ "cursor": function(idbCursor, callback) { >+ return $.Deferred(function(dfd) { >+ try { >+ >+ var cursorReq = typeof idbCursor === "function" ? idbCursor() : idbCursor; >+ cursorReq.onsuccess = function(e) { >+ >+ if (!cursorReq.result) { >+ dfd.resolveWith(cursorReq, [null, e]); >+ return; >+ } >+ var elem = { >+ // Delete, update do not move >+ "delete": function() { >+ return wrap.request(function() { >+ return cursorReq.result["delete"](); >+ }); >+ }, >+ "update": function(data) { >+ return wrap.request(function() { >+ return cursorReq.result["update"](data); >+ }); >+ }, >+ "next": function(key) { >+ this.data = key; >+ }, >+ "key": cursorReq.result.key, >+ "value": cursorReq.result.value >+ }; >+ >+ dfd.notifyWith(cursorReq, [elem, e]); >+ var result = callback.apply(cursorReq, [elem]); >+ >+ try { >+ if (result === false) { >+ dfd.resolveWith(cursorReq, [null, e]); >+ } else if (typeof result === "number") { >+ cursorReq.result["advance"].apply(cursorReq.result, [result]); >+ } else { >+ if (elem.data) cursorReq.result["continue"].apply(cursorReq.result, [elem.data]); >+ else cursorReq.result["continue"](); >+ } >+ } catch (e) { >+ >+ dfd.rejectWith(cursorReq, [cursorReq.result, e]); >+ } >+ }; >+ cursorReq.onerror = function(e) { >+ >+ dfd.rejectWith(cursorReq, [cursorReq.result, e]); >+ }; >+ } catch (e) { >+ >+ e.type = "exception"; >+ dfd.rejectWith(cursorReq, [null, e]); >+ } >+ }); >+ }, >+ >+ "index": function(index) { >+ try { >+ var idbIndex = (typeof index === "function" ? index() : index); >+ } catch (e) { >+ idbIndex = null; >+ } >+ >+ return { >+ "each": function(callback, range, direction) { >+ return wrap.cursor(function() { >+ if (direction) { >+ return idbIndex.openCursor(wrap.range(range), direction); >+ } else { >+ return idbIndex.openCursor(wrap.range(range)); >+ } >+ >+ }, callback); >+ }, >+ "eachKey": function(callback, range, direction) { >+ return wrap.cursor(function() { >+ if (direction) { >+ return idbIndex.openKeyCursor(wrap.range(range), direction); >+ } else { >+ return idbIndex.openKeyCursor(wrap.range(range)); >+ } >+ }, callback); >+ }, >+ "get": function(key) { >+ if (typeof idbIndex.get === "function") { >+ return wrap.request(idbIndex.get(key)); >+ } else { >+ return idbIndex.openCursor(wrap.range(key)); >+ } >+ }, >+ "count": function() { >+ if (typeof idbIndex.count === "function") { >+ return wrap.request(idbIndex.count()); >+ } else { >+ throw "Count not implemented for cursors"; >+ } >+ }, >+ "getKey": function(key) { >+ if (typeof idbIndex.getKey === "function") { >+ return wrap.request(idbIndex.getKey(key)); >+ } else { >+ return idbIndex.openKeyCursor(wrap.range(key)); >+ } >+ } >+ }; >+ } >+ }; >+ >+ >+ // Start with opening the database >+ var dbPromise = wrap.request(function() { >+ >+ return version ? indexedDB.open(dbName, parseInt(version)) : indexedDB.open(dbName); >+ }); >+ dbPromise.then(function(db, e) { >+ >+ db.onversionchange = function() { >+ // Try to automatically close the database if there is a version change request >+ if (!(config && config.onversionchange && config.onversionchange() !== false)) { >+ db.close(); >+ } >+ }; >+ }, function(error, e) { >+ >+ // Nothing much to do if an error occurs >+ }, function(db, e) { >+ if (e && e.type === "upgradeneeded") { >+ if (config && config.schema) { >+ // Assuming that version is always an integer >+ >+ for (var i = e.oldVersion + 1; i <= e.newVersion; i++) { >+ typeof config.schema[i] === "function" && config.schema[i].call(this, wrap.transaction(this.transaction)); >+ } >+ } >+ if (config && typeof config.upgrade === "function") { >+ config.upgrade.call(this, wrap.transaction(this.transaction)); >+ } >+ } >+ }); >+ >+ return $.extend(dbPromise, { >+ "cmp": function(key1, key2) { >+ return indexedDB.cmp(key1, key2); >+ }, >+ "deleteDatabase": function() { >+ // Kinda looks ugly coz DB is opened before it needs to be deleted. >+ // Blame it on the API >+ return $.Deferred(function(dfd) { >+ dbPromise.then(function(db, e) { >+ db.close(); >+ wrap.request(function() { >+ return indexedDB.deleteDatabase(dbName); >+ }).then(function(result, e) { >+ dfd.resolveWith(this, [result, e]); >+ }, function(error, e) { >+ dfd.rejectWith(this, [error, e]); >+ }, function(db, e) { >+ dfd.notifyWith(this, [db, e]); >+ }); >+ }, function(error, e) { >+ dfd.rejectWith(this, [error, e]); >+ }, function(db, e) { >+ dfd.notifyWith(this, [db, e]); >+ }); >+ }); >+ }, >+ "transaction": function(storeNames, mode) { >+ !$.isArray(storeNames) && (storeNames = [storeNames]); >+ mode = getDefaultTransaction(mode); >+ return $.Deferred(function(dfd) { >+ dbPromise.then(function(db, e) { >+ var idbTransaction; >+ try { >+ >+ idbTransaction = db.transaction(storeNames, mode); >+ >+ idbTransaction.onabort = idbTransaction.onerror = function(e) { >+ dfd.rejectWith(idbTransaction, [e]); >+ }; >+ idbTransaction.oncomplete = function(e) { >+ dfd.resolveWith(idbTransaction, [e]); >+ }; >+ } catch (e) { >+ >+ e.type = "exception"; >+ dfd.rejectWith(this, [e]); >+ return; >+ } >+ try { >+ dfd.notifyWith(idbTransaction, [wrap.transaction(idbTransaction)]); >+ } catch (e) { >+ e.type = "exception"; >+ dfd.rejectWith(this, [e]); >+ } >+ }, function(err, e) { >+ dfd.rejectWith(this, [e, err]); >+ }, function(res, e) { >+ >+ //dfd.notifyWith(this, ["", e]); >+ }); >+ >+ }); >+ }, >+ "objectStore": function(storeName, mode) { >+ var me = this, >+ result = {}; >+ >+ function op(callback) { >+ return $.Deferred(function(dfd) { >+ function onTransactionProgress(trans, callback) { >+ try { >+ >+ callback(trans.objectStore(storeName)).then(function(result, e) { >+ dfd.resolveWith(this, [result, e]); >+ }, function(err, e) { >+ dfd.rejectWith(this, [err, e]); >+ }); >+ } catch (e) { >+ >+ e.name = "exception"; >+ dfd.rejectWith(trans, [e, e]); >+ } >+ } >+ me.transaction(storeName, getDefaultTransaction(mode)).then(function() { >+ >+ // Nothing to do when transaction is complete >+ }, function(err, e) { >+ // If transaction fails, CrudOp fails >+ if (err.code === err.NOT_FOUND_ERR && (mode === true || typeof mode === "object")) { >+ >+ var db = this.result; >+ db.close(); >+ dbPromise = wrap.request(function() { >+ >+ return indexedDB.open(dbName, (parseInt(db.version, 10) || 1) + 1); >+ }); >+ dbPromise.then(function(db, e) { >+ >+ db.onversionchange = function() { >+ // Try to automatically close the database if there is a version change request >+ if (!(config && config.onversionchange && config.onversionchange() !== false)) { >+ db.close(); >+ } >+ }; >+ me.transaction(storeName, getDefaultTransaction(mode)).then(function() { >+ >+ // Nothing much to do >+ }, function(err, e) { >+ dfd.rejectWith(this, [err, e]); >+ }, function(trans, e) { >+ >+ onTransactionProgress(trans, callback); >+ }); >+ }, function(err, e) { >+ dfd.rejectWith(this, [err, e]); >+ }, function(db, e) { >+ if (e.type === "upgradeneeded") { >+ try { >+ >+ db.createObjectStore(storeName, mode === true ? { >+ "autoIncrement": true >+ } : mode); >+ >+ } catch (ex) { >+ >+ dfd.rejectWith(this, [ex, e]); >+ } >+ } >+ }); >+ } else { >+ dfd.rejectWith(this, [err, e]); >+ } >+ }, function(trans) { >+ >+ onTransactionProgress(trans, callback); >+ }); >+ }); >+ } >+ >+ function crudOp(opName, args) { >+ return op(function(wrappedObjectStore) { >+ return wrappedObjectStore[opName].apply(wrappedObjectStore, args); >+ }); >+ } >+ >+ function indexOp(opName, indexName, args) { >+ return op(function(wrappedObjectStore) { >+ var index = wrappedObjectStore.index(indexName); >+ return index[opName].apply(index[opName], args); >+ }); >+ } >+ >+ var crud = ["add", "delete", "get", "put", "clear", "count", "each"]; >+ for (var i = 0; i < crud.length; i++) { >+ result[crud[i]] = (function(op) { >+ return function() { >+ return crudOp(op, arguments); >+ }; >+ })(crud[i]); >+ } >+ >+ result.index = function(indexName) { >+ return { >+ "each": function(callback, range, direction) { >+ return indexOp("each", indexName, [callback, range, direction]); >+ }, >+ "eachKey": function(callback, range, direction) { >+ return indexOp("eachKey", indexName, [callback, range, direction]); >+ }, >+ "get": function(key) { >+ return indexOp("get", indexName, [key]); >+ }, >+ "count": function() { >+ return indexOp("count", indexName, []); >+ }, >+ "getKey": function(key) { >+ return indexOp("getKey", indexName, [key]); >+ } >+ }; >+ }; >+ >+ return result; >+ } >+ }); >+ } >+ }); >+ >+ $.indexedDB.IDBCursor = IDBCursor; >+ $.indexedDB.IDBTransaction = IDBTransaction; >+ $.idb = $.indexedDB; >+})(jQuery); >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >index 3439e23..38fb53d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >+++ b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >@@ -2567,4 +2567,28 @@ fieldset.rows table.mceListBox { > font-size: 140%; > font-family : monospace; > padding : .3em; >-} >\ No newline at end of file >+} >+ >+.loading-overlay { >+ background-color: #FFF; >+ cursor: wait; >+ height: 100%; >+ left: 0; >+ opacity: .7; >+ position: fixed; >+ top: 0; >+ width: 100%; >+ z-index: 1000; >+} >+.loading-overlay div { >+ background : transparent url(../../img/loading.gif) top left no-repeat; >+ font-size : 175%; >+ font-weight: bold; >+ height: 2em; >+ left: 50%; >+ margin: -1em 0 0 -2.5em; >+ padding-left : 50px; >+ position: absolute; >+ top: 50%; >+ width: 15em; >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/offlinecirc.js b/koha-tmpl/intranet-tmpl/prog/en/js/offlinecirc.js >new file mode 100644 >index 0000000..4645d17 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/js/offlinecirc.js >@@ -0,0 +1,109 @@ >+/* Copyright 2013 C & P Bibliography Services >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it under the >+ * terms of the GNU General Public License as published by the Free Software >+ * Foundation; either version 3 of the License, or (at your option) any later >+ * version. >+ * >+ * Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License along >+ * with Koha; if not, write to the Free Software Foundation, Inc., >+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ */ >+ >+(function( kohadb, $, undefined ) { >+ kohadb.settings = kohadb.settings || {}; >+ kohadb.initialize = function (callback) { >+ $.indexedDB("koha", { >+ "version": 1, >+ "schema": { >+ "1": function(versionTransaction){ >+ var patrons = versionTransaction.createObjectStore("patrons", { >+ "keyPath": "cardnumber" >+ }); >+ var items = versionTransaction.createObjectStore("items", { >+ "keyPath": "barcode" >+ }); >+ var issues = versionTransaction.createObjectStore("issues", { >+ "keyPath": "barcode" >+ }); >+ issues.createIndex("cardnumber", { "multiEntry": true }); >+ var transactions = versionTransaction.createObjectStore("transactions", { >+ "keyPath": "timestamp" >+ }); >+ var settings = versionTransaction.createObjectStore("offline_settings", { >+ "keyPath": "key" >+ }); >+ }, >+ } >+ }).done(function(){ >+ if (typeof callback === 'function') { >+ callback(); >+ kohadb.loadSetting('userid'); >+ kohadb.loadSetting('branchcode'); >+ } >+ }); >+ }; >+ kohadb.loadSetting = function (key, callback) { >+ $.indexedDB("koha").transaction(["offline_settings"]).then(function(){ >+ }, function(err, e){ >+ }, function(transaction){ >+ var settings = transaction.objectStore("offline_settings"); >+ settings.get(key).done(function (item, error) { >+ if (typeof item !== 'undefined') { >+ kohadb.settings[key] = item.value; >+ } >+ if (typeof callback === 'function') { >+ callback(key, kohadb.settings[key]); >+ } >+ }); >+ }); >+ }; >+ kohadb.saveSetting = function (key, value) { >+ $.indexedDB("koha").transaction(["offline_settings"]).then(function(){ >+ }, function(err, e){ >+ }, function(transaction){ >+ var settings = transaction.objectStore("offline_settings"); >+ settings.put({ "key" : key, "value" : value }); >+ kohadb.settings[key] = value; >+ }); >+ }; >+ kohadb.recordTransaction = function (newtrans, callback) { >+ $.indexedDB("koha").transaction(["transactions"]).then(function(){ >+ callback(newtrans); >+ }, function(err, e){ >+ }, function(dbtransaction) { >+ var transactions = dbtransaction.objectStore("transactions"); >+ transactions.put(newtrans); >+ }); >+ }; >+}( window.kohadb = window.bndb || {}, jQuery )); >+ >+if ( !Date.prototype.toMySQLString ) { >+ ( function() { >+ >+ function pad(number) { >+ var r = String(number); >+ if ( r.length === 1 ) { >+ r = '0' + r; >+ } >+ return r; >+ } >+ >+ Date.prototype.toMySQLString = function() { >+ return this.getFullYear() >+ + '-' + pad( this.getMonth() + 1 ) >+ + '-' + pad( this.getDate() ) >+ + ' ' + pad( this.getHours() ) >+ + ':' + pad( this.getMinutes() ) >+ + ':' + pad( this.getSeconds() ) >+ + '.' + String( (this.getMilliseconds()/1000).toFixed(3) ).slice( 2, 5 ); >+ }; >+ >+ }() ); >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >index 64cf338..91b2264 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >@@ -123,6 +123,12 @@ Circulation: > - The following fields should be excluded from the patron checkout history CSV or iso2709 export > - pref: ExportRemoveFields > - (separate fields with space, e.g. 100a 200b 300c) >+ - >+ - pref: AllowOfflineCirculation >+ choices: >+ yes: Enable >+ no: "Do not enable" >+ - "offline circulation on regular circulation computers. (NOTE: This system preference does not affect the Firefox plugin or the desktop application)" > > Checkout Policy: > - >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt >index af533f4..72abd1f 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt >@@ -53,12 +53,15 @@ > <div class="yui-u"> > <h5>Offline circulation</h5> > <ul> >- <li><a href="/cgi-bin/koha/offline_circ/process_koc.pl">Upload offline circulation file (.koc)</a></li> >- <li><a href="/cgi-bin/koha/offline_circ/list.pl">Pending offline circulation actions</a> >- <ul> >- <li><a href="http://kylehall.info/index.php/projects/koha/koha-offline-circulation/">Get desktop application</a></li> >- <li><a href="https://addons.mozilla.org/[% lang %]/firefox/addon/koct/">Get Firefox add-on</a></li> >- </ul> >+ [% IF (AllowOfflineCirculation) %] >+ <li><a href="/cgi-bin/koha/circ/offline.pl">Built-in offline circulation interface</a></li> >+ [% END %] >+ <li><a href="/cgi-bin/koha/offline_circ/process_koc.pl">Upload offline circulation file (.koc)</a></li> >+ <li><a href="/cgi-bin/koha/offline_circ/list.pl">Pending offline circulation actions</a> >+ <ul> >+ <li><a href="http://kylehall.info/index.php/projects/koha/koha-offline-circulation/">Get desktop application</a></li> >+ <li><a href="https://addons.mozilla.org/[% lang %]/firefox/addon/koct/">Get Firefox add-on</a></li> >+ </ul> > </ul> > </div> > </div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline-mf.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline-mf.tt >new file mode 100644 >index 0000000..93a0259 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline-mf.tt >@@ -0,0 +1,43 @@ >+CACHE MANIFEST >+# [% cookie %] >+ >+# Explicitly cached 'master entries'. >+CACHE: >+/cgi-bin/koha/circ/offline.pl >+/intranet-tmpl/lib/bootstrap/bootstrap.min.css >+/intranet-tmpl/lib/bootstrap/bootstrap.min.js >+/intranet-tmpl/lib/jquery/jquery-ui.css >+/intranet-tmpl/lib/jquery/jquery-ui.js >+/intranet-tmpl/lib/jquery/jquery.js >+/intranet-tmpl/lib/jquery/plugins/jquery.cookie.min.js >+/intranet-tmpl/lib/jquery/plugins/jquery.highlight-3.js >+/intranet-tmpl/lib/jquery/plugins/jquery.hotkeys.min.js >+/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js >+/intranet-tmpl/lib/jquery/plugins/jquery.validate.min.js >+/intranet-tmpl/prog/en/css/print.css >+/intranet-tmpl/prog/en/css/staff-global.css >+/intranet-tmpl/prog/en/js/basket.js >+/intranet-tmpl/prog/en/js/offlinecirc.js >+/intranet-tmpl/prog/en/js/staff-global.js >+/intranet-tmpl/prog/en/lib/jquery/plugins/jquery-ui-timepicker-addon.js >+/intranet-tmpl/prog/en/lib/yui/button/button-min.js >+/intranet-tmpl/prog/en/lib/yui/container/container_core-min.js >+/intranet-tmpl/prog/en/lib/yui/menu/menu-min.js >+/intranet-tmpl/prog/en/lib/yui/reset-fonts-grids.css >+/intranet-tmpl/prog/en/lib/yui/skin.css >+/intranet-tmpl/prog/en/lib/yui/utilities/utilities.js >+/intranet-tmpl/prog/img/cart-small.gif >+/intranet-tmpl/prog/img/glyphicons-halflings-koha.png >+/intranet-tmpl/prog/img/koha-logo-medium.gif >+/intranet-tmpl/prog/img/loading.gif >+/intranet-tmpl/prog/sound/beep.ogg >+/intranet-tmpl/prog/sound/critical.ogg >+ >+# Resources that require the user to be online. >+NETWORK: >+* >+ >+# static.html will be served if main.py is inaccessible >+# offline.jpg will be served in place of all images in images/large/ >+# offline.html will be served in place of all other .html files >+FALLBACK: >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline.tt >new file mode 100644 >index 0000000..772d9db >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/offline.tt >@@ -0,0 +1,690 @@ >+<!DOCTYPE html> >+[% IF (AllowOfflineCirculation) %] >+[% SET manifestattr = 'manifest="/cgi-bin/koha/circ/offline-mf.pl"' %] >+[% END %] >+[% IF ( bidi ) %]<html lang="[% lang %]" dir="[% bidi %]" [% manifestattr %]>[% ELSE %]<html lang="[% lang %]" [% manifestattr %]>[% END %] >+<head> >+<title>Koha › Circulation</title> >+[% INCLUDE 'doc-head-close.inc' %] >+<script type="text/javascript" src="/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js"></script> >+<script type="text/javascript" src="/intranet-tmpl/prog/en/js/offlinecirc.js"></script> >+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery-ui-timepicker-addon.js"></script> >+<script type="text/javascript"> >+//<![CDATA[ >+var ALERT_MATERIALS = _("Note about the accompanying materials: "); >+var ALERT_RESTRICTED = _("Patron is RESTRICTED"); >+var ALERT_NO_MATCHING_ITEM = _("No item with barcode in offline database (transaction recorded anyway): "); >+var ALERT_NOT_CHECKED_OUT = _("Item not listed as checked out in offline database (transaction recorded anyway)"); >+var ALERT_ITEM_WITHDRAWN = _("Item has been withdrawn (transaction recorded anyway)"); >+var ALERT_ITEM_RESTRICTED = _("Item is restricted (transaction recorded anyway)"); >+var ALERT_ITEM_LOST = _("Item is has been lost (transaction recorded anyway)"); >+var ALERT_NO_MATCHING_PATRON = _("No patron cardnumber in offline database (proceeding anyway): "); >+var ALERT_PATRON_GONE_NO_ADDRESS = _("Patron's address is in doubt (transaction recorded anyway)"); >+var ALERT_PATRON_CARD_LOST = _("Patron's card is lost"); >+var ALERT_PATRON_EXPIRED = _("Patron's card is expired"); >+var ALERT_PATRON_BLOCKED_TEMPORARY = _("Patron has had overdue items and is restricted for: "); >+var ALERT_PATRON_RESTRICTED = _("Patron is restricted"); >+var ALERT_PATRON_FINE = _("Patron has outstanding fines: "); >+var ALERT_PATRON_FINE_OVER_LIMIT = _("Patron fines are over limit: "); >+ >+var start; >+ >+var dateformat = '[% IF ( dateformat_us ) %]mm/dd/yy[% ELSIF ( dateformat_metric ) %]dd/mm/yy[% ELSE %]yy-mm-dd[% END %]'; >+ >+function checkin(barcode, item, error) { >+ var alerts = checkAlerts(barcode, item); >+ if (typeof item === 'undefined') { >+ item = { }; >+ } >+ item.title = item.title || _("(Unknown)"); >+ item.author = item.author || _("(Unknown)"); >+ item.homebranch = item.homebranch || ""; >+ item.holdingbranch = item.holdingbranch || ""; >+ item.callnumber = item.callnumber || ""; >+ item.itemtype = item.itemtype || ""; >+ item.barcode = item.barcode || barcode; >+ var trans = { "timestamp" : new Date().toMySQLString(), >+ "barcode" : barcode, >+ "action" : "return" >+ }; >+ $('#alerts').empty(); >+ $('#offline-home').hide(); >+ $('#offline-returns').show(); >+ kohadb.recordTransaction(trans, function () { >+ $('#already-checked-in tbody').prepend('<tr><td>' + item.title + '</td><td>' + item.author + '</td><td>' + barcode + '</td><td>' + item.homebranch + '</td><td>' + item.holdingbranch + '</td><td></td><td>' + item.callnumber + '</td><td>' + item.itemtype + '</td></tr>'); >+ if (alerts.length > 0) { >+ $('#alerts').append('<div class="dialog alert"><h3>' + _("Check in message") + '</h3></div>'); >+ for (var msg in alerts) { >+ $('#alerts .dialog').append('<p>' + alerts[msg] + '</p'); >+ } >+ } >+ }); >+} >+ >+function checkAlerts(barcode, item) { >+ var alerts = []; >+ if (typeof item === 'undefined') { >+ alerts.push(ALERT_NO_MATCHING_ITEM + barcode); >+ } else { >+ if (typeof item.materials !== 'undefined' && item.materials != null) { >+ alerts.push(ALERT_MATERIALS + item.materials); >+ } >+ } >+ return alerts; >+} >+ >+function synchronize() { >+ kohadb.saveSetting("userid", "[% loggedinusername %]"); >+ kohadb.saveSetting("branchcode", "[% LoginBranchcode %]"); >+ kohadb.loadSetting("item-timestamp", showTimestamp); >+ kohadb.loadSetting("patron-timestamp", showTimestamp); >+ kohadb.loadSetting("issue-timestamp", showTimestamp); >+ [% UNLESS (AllowOfflineCirculation) %] >+ reloadRecords(); >+ [% END %] >+ $('#download-records').click(reloadRecords); >+ $('#upload-transactions').click(function () { >+ $('.loading-overlay div').text(_("Uploading transactions, please wait...")); >+ $('.loading-overlay').show(); >+ var uploadIter = $.indexedDB("koha").objectStore("transactions").each(uploadTransaction); >+ uploadIter.done(function() { >+ $('.loading-overlay').hide(); >+ }); >+ }); >+ >+} >+ >+function showTimestamp(key, value) { >+ if (typeof value !== 'undefined') { >+ var ts = new Date(value); >+ $('#' + key).text($.datepicker.formatDate(dateformat, ts) + ' ' + ts.toTimeString()); >+ } else { >+ $('#' + key).text(_("(never)")); >+ } >+} >+ >+function reloadRecords(ev) { >+ $(".loading-overlay div").text(_("Loading records, please wait...")); >+ $(".loading-overlay").show(); >+ start = new Date(); >+ $.indexedDB("koha").transaction(["patrons", "items", "issues"]).then(function(){ >+ loadRecords(0); >+ }, function(err, e){ >+ }, function(transaction){ >+ transaction.objectStore("patrons").clear(); >+ transaction.objectStore("items").clear(); >+ transaction.objectStore("issues").clear(); >+ }); >+ if (typeof ev !== 'undefined') { >+ ev.stopPropagation(); >+ } >+} >+ >+function uploadTransaction(transaction) { >+ $.ajax({ >+ type: "GET", >+ url: "/cgi-bin/koha/offline_circ/service.pl", >+ data: { "userid" : kohadb.settings.userid, >+ "branchcode" : kohadb.settings.branchcode, >+ "timestamp" : transaction.value.timestamp, >+ "action" : transaction.value.action, >+ "barcode" : transaction.value.barcode, >+ "cardnumber" : transaction.value.cardnumber, >+ "pending" : true, >+ }, >+ }).done(function () { >+ transaction.delete(); >+ }); >+} >+ >+function finishedLoading() { >+ kohadb.saveSetting('item-timestamp', start.toISOString()) >+ kohadb.saveSetting('patron-timestamp', start.toISOString()) >+ kohadb.saveSetting('issue-timestamp', start.toISOString()) >+ showTimestamp('item-timestamp', start.toISOString()); >+ showTimestamp('patron-timestamp', start.toISOString()); >+ showTimestamp('issue-timestamp', start.toISOString()); >+ $(".loading-overlay").hide(); >+} >+ >+function loadRecords(page) { >+[% IF (AllowOfflineCirculation) %] >+ $(".loading-overlay div").text(_("Loading page " + page + ", please wait...")); >+ $(".loading-overlay").show(); >+ $.ajax({ >+ type: "GET", >+ url: "/cgi-bin/koha/offline_circ/download.pl", >+ data: { "data": "all", >+ "page": page >+ }, >+ dataType: "json", >+ }).done(function (data) { >+ $.indexedDB("koha").transaction(["patrons", "items", "issues"]).then(function(){ >+ if ($.isEmptyObject(data.patrons) && $.isEmptyObject(data.items)) { >+ finishedLoading(); >+ } else { >+ setTimeout(function () { loadRecords(page + 1); }, 200); >+ } >+ }, function(err, e){ >+ }, function(transaction){ >+ if (data.patrons) { >+ var patrons = transaction.objectStore("patrons"); >+ $.each(data.patrons, function () { >+ patrons.put(this); >+ }); >+ } >+ if (data.items) { >+ var items = transaction.objectStore("items"); >+ $.each(data.items, function () { >+ items.put(this); >+ }); >+ } >+ if (data.issues) { >+ var issues = transaction.objectStore("issues"); >+ $.each(data.issues, function () { >+ issues.put(this); >+ }); >+ } >+ }); >+ }); >+[% END %] >+} >+ >+function validate1(date) { >+ var today = new Date(); >+ if ( date < today ) { >+ return true; >+ } else { >+ return false; >+ } >+}; >+ >+function loadPatron(barcode) { >+ $('#oldissues').hide(); >+ $('#session-issues').hide(); >+ $('#session-payments').hide(); >+ $.indexedDB("koha").transaction(["patrons", "issues"]).then(function() { >+ }, function(err, e){ >+ }, function(transaction){ >+ var patrons = transaction.objectStore("patrons"); >+ patrons.get(barcode).done(function (patron, error) { >+ showPatron(barcode, patron, error); >+ }); >+ var issuesidx = transaction.objectStore("issues").index("cardnumber"); >+ $('#oldissuest tbody').empty(); >+ issuesidx.each(function (item) { >+ $('#oldissues').show(); >+ $('#oldissuest tbody').append("<tr><td>" + item.value.date_due + "</td><td>" + item.value.barcode + "</td><td>" + item.value.title + "</td><td>" + item.value.itype + "</td><td>" + item.value.issuedate + "</td><td>" + item.value.issuebranch + "</td><td>" + item.value.callnumber + "</td><td>" + "" + "</td></tr>"); >+ }, barcode); >+ }); >+} >+ >+function checkout(barcode, item, error) { >+ var alerts = checkAlerts(barcode, item); >+ if (typeof item === 'undefined') { >+ item = { }; >+ } >+ item.title = item.title || ""; >+ item.author = item.author || ""; >+ item.homebranch = item.homebranch || ""; >+ item.holdingbranch = item.holdingbranch || ""; >+ item.callnumber = item.callnumber || ""; >+ item.itemtype = item.itemtype || ""; >+ if ($('#duedatespec').val().length === 0) { >+ alert(_("You must set a due date in order to use offline circulation!")); >+ $('#duedatespec').focus(); >+ return; >+ } >+ var date_due = new Date($('#duedatespec').datepicker('getDate')); >+ var trans = { "timestamp" : new Date().toMySQLString(), >+ "barcode" : barcode, >+ "cardnumber" : curpatron.cardnumber, >+ "date_due" : date_due.toMySQLString(), >+ "action" : "issue" >+ }; >+ $('#alerts').empty(); >+ kohadb.recordTransaction(trans, function () { >+ $('#session-issues').show(); >+ $('#issuest tbody').prepend('<tr><td>' + $.datepicker.formatDate(dateformat, date_due) + date_due.toTimeString() + '</td><td>' + item.title + '</td><td>' + barcode + '</td><td>' + item.itemtype + '</td><td>' + $.datepicker.formatDate(dateformat, new Date()) + '</td><td>' + kohadb.settings.branchcode + '</td><td>' + item.callnumber + '</td><td></td></tr>'); >+ if (alerts.length > 0) { >+ $('#alerts').append('<div class="dialog alert"><h3>' + _("Check out message") + '</h3></div>'); >+ for (var msg in alerts) { >+ $('#alerts .dialog').append('<p>' + alerts[msg] + '</p'); >+ } >+ } >+ }); >+} >+ >+function recordFine(amount) { >+ var timestamp = new Date() >+ var trans = { "timestamp" : timestamp.toMySQLString(), >+ "cardnumber" : curpatron.cardnumber, >+ "amount" : amount, >+ "action" : "payment", >+ }; >+ kohadb.recordTransaction(trans, function () { >+ $('#session-payments').show(); >+ $('#session-payments tbody').prepend('<tr><td>' + amount + '</td><td>' + $.datepicker.formatDate(dateformat, timestamp) + timestamp.toTimeString() + '</td></tr>'); >+ }); >+} >+ >+function checkPatronAlerts(cardnumber, patron) { >+ var alerts = []; >+ if (typeof patron === 'undefined') { >+ alerts.push(ALERT_NO_MATCHING_PATRON + cardnumber); >+ } else { >+ if (patron.gonenoaddress !== '0') { >+ alerts.push(ALERT_PATRON_GONE_NO_ADDRESS); >+ } >+ if (patron.lost !== '0') { >+ alerts.push(ALERT_PATRON_CARD_LOST); >+ } >+ if (patron.debarred !== null) { >+ if (patron.debarred != '9999-12-31') { >+ alerts.push(ALERT_PATRON_BLOCKED_TEMPORARY + $.datepicker.formatDate(dateformat, patron.debarred)); >+ } else { >+ alerts.push(ALERT_PATRON_RESTRICTED); >+ } >+ } >+ if (parseInt(patron.fine) > [% maxoutstanding %]) { >+ alerts.push(ALERT_PATRON_FINE_OVER_LIMIT + patron.fine); >+ } else if (parseInt(patron.fine) > 0) { >+ alerts.push(ALERT_PATRON_FINE + patron.fine); >+ } >+ } >+ return alerts; >+} >+ >+var curpatron; >+ >+function showPatron(barcode, patron, error) { >+ var alerts = checkPatronAlerts(barcode, patron); >+ if (typeof patron === 'undefined') { >+ patron = { }; >+ } >+ patron.surname = patron.surname || ""; >+ patron.firstname = patron.firstname || ""; >+ patron.othernames = patron.othernames || ""; >+ patron.address = patron.address || ""; >+ patron.address2 = patron.address2 || ""; >+ patron.city = patron.city || ""; >+ patron.state = patron.state || ""; >+ patron.country = patron.country || ""; >+ patron.zipcode = patron.zipcode || ""; >+ patron.phone = patron.phone || ""; >+ patron.mobile = patron.mobile || ""; >+ patron.phonepro = patron.phonepro || ""; >+ patron.email = patron.email || ""; >+ patron.emailpro = patron.emailpro || ""; >+ patron.categorycode = patron.categorycode || ""; >+ patron.branchcode = patron.branchcode || ""; >+ patron.cardnumber = barcode; >+ patron.fine = patron.fine || "0"; >+ >+ patron.name = patron.firstname + (patron.othernames.length > 0 ? " (" + patron.othernames + ") " : " ") + patron.surname + " (" + barcode + ")"; >+ if (patron.name.length > 0) { >+ $('.patron-title').text(patron.name); >+ } else { >+ $('.patron-title').text(_("Unrecognized patron") + " (" + barcode + ")"); >+ } >+ if (patron.address.length > 0 || patron.address2.length > 0) { >+ $('#patron-address-1').text(patron.address); >+ $('#patron-address-2').text(patron.address2); >+ } else { >+ $('#patron-address-1').html('<span class="empty" id="noaddressstored">' + _("No address stored.") + '</span></li>'); >+ $('#patron-address-2').text(''); >+ } >+ if (patron.city.length > 0) { >+ $('#patron-address-parts').text(patron.city + (patron.state.length > 0 ? ", " + patron.state : "") + " " + patron.zipcode + (patron.country.length > 0 ? ", " + patron.country : "")); >+ } else { >+ $('#patron-address-parts').html('<span class="empty" id="nocitystored">' + _("No city stored.") + '</span></li>'); >+ } >+ if (patron.phone.length > 0 || patron.mobile.length > 0 || patron.phonepro.length > 0) { >+ $('#patron-phone').text((patron.phone.length > 0 ? patron.phone : (patron.mobile.length > 0 ? patron.mobile : (patron.phonepro.length > 0 ? patron.phonepro : '')))); >+ } else { >+ $('#patron-phone').html('<span class="empty" id="nophonestored">' + _("No phone stored.") + '</span></li>'); >+ } >+ if (patron.email.length > 0 || patron.emailpro.length > 0) { >+ $('#patron-email').text((patron.email.length > 0 ? patron.email : (patron.emailpro.length > 0 ? patron.emailpro : ""))); >+ } else { >+ $('#patron-email').html('<span class="empty" id="noemailstored">' + _("No email stored.") + '</span></li>'); >+ } >+ if (patron.categorycode.length > 0) { >+ $('#patron-category').text(_("Category: ") + patron.categorycode); >+ } else { >+ $('#patron-category').html('<span class="empty" id="unknowncategory">' + _("Category code unknown.") + '</span></li>'); >+ } >+ if (patron.branchcode.length > 0) { >+ $('#patron-library').text(_("Home library: ") + patron.branchcode); >+ } else { >+ $('#patron-library').html('<span class="empty" id="unknowncategory">' + _("Home library unknown.") + '</span></li>'); >+ } >+ $('.fine-amount').text(patron.fine); >+ $('#alerts').empty(); >+ if (alerts.length > 0) { >+ $('#alerts').append('<div class="dialog alert"><h3>' + _("Check out message") + '</h3></div>'); >+ for (var msg in alerts) { >+ $('#alerts .dialog').append('<p>' + alerts[msg] + '</p'); >+ } >+ } >+ curpatron = patron; >+ $('#yui-main').show(); >+ $('#barcode').focus(); >+} >+ >+// This next bit of code is to deal with the updated session issue >+window.addEventListener('load', function(e) { >+ window.applicationCache.addEventListener('updateready', function(e) { >+ if (window.applicationCache.status == window.applicationCache.UPDATEREADY) { >+ // Browser downloaded a new app cache. >+ // Swap it in and reload the page to get the new hotness. >+ window.applicationCache.swapCache(); >+ if (confirm(_("A new version of this site is available. Load it?"))) { >+ window.location.reload(); >+ } >+ } else { >+ // Manifest didn't changed. Nothing new to server. >+ } >+ }, false); >+}, false); >+ >+ >+$(document).ready(function () { >+ kohadb.initialize(); >+ >+ // Returns code >+ $('#checkin-form, #checkin_search form').submit(function (event) { >+ event.preventDefault(); >+ var barcode = $('input[name="barcode"]', this).val(); >+ $('input[name="barcode"]', this).val(''); >+ $.indexedDB("koha").transaction(["items"]).then(function() { >+ }, function(err, e){ >+ }, function(transaction){ >+ var items = transaction.objectStore("items"); >+ items.get(barcode).done(function (item, error) { >+ checkin(barcode, item, error); >+ }); >+ }); >+ }); >+ >+ $('#go-to-home').click(function () { >+ $('.offline-sync').hide(); >+ $('.offline-circulation').hide(); >+ $('.offline-returns').hide(); >+ $('.offline-home').show(); >+ }); >+ >+ $('#go-to-returns').click(function () { >+ $('.offline-home').hide(); >+ $('.offline-sync').hide(); >+ $('.offline-circulation').hide(); >+ $('.offline-returns').show(); >+ $('#checkin-form input[name="barcode"]').focus(); >+ }); >+ >+ $('#go-to-circ').click(function () { >+ $('.offline-home').hide(); >+ $('.offline-sync').hide(); >+ $('.offline-returns').hide(); >+ $('.offline-circulation').hide(); >+ $('#header_search').tabs("option", "active", 0); >+ $('#circ_search input[name="findborrower"]').focus(); >+ }); >+ >+ $('#go-to-sync').click(function () { >+ $.ajax({ >+ type: "GET", >+ url: "/cgi-bin/koha/offline_circ/list.pl", >+ success: function () { >+ $('.offline-home').hide(); >+ $('.offline-returns').hide(); >+ $('.offline-circulation').hide(); >+ $('.offline-sync').show(); >+ synchronize(); >+ }, >+ error: function () { >+ alert(_("You are offline and therefore cannot sync your database")); >+ } >+ }); >+ }); >+ >+ $('#patronsearch').submit(function (event) { >+ event.preventDefault(); >+ loadPatron($('#findborrower').val()); >+ $('.offline-home').hide(); >+ $('.offline-returns').hide(); >+ $('.offline-sync').hide(); >+ $('.offline-circulation').show(); >+ $('#findborrower').val(''); >+ $('#barcode').focus(); >+ }); >+ >+ $('#pay-fine').click(function (event) { >+ event.preventDefault(); >+ recordFine($('#pay-fine-amount').val()); >+ }); >+ >+ $('#patronlists').tabs(); >+ >+ $("#newduedate").datetimepicker({ >+ minDate: 1, // require that renewal date is after today >+ hour: 23, >+ minute: 59 >+ }); >+ $("#duedatespec").datetimepicker({ >+ onClose: function(dateText, inst) { $("#barcode").focus(); }, >+ hour: 23, >+ minute: 59 >+ }); >+ $('#mainform').submit(function (event) { >+ event.preventDefault(); >+ var barcode = $('#barcode').val(); >+ $.indexedDB("koha").transaction(["items"]).then(function() { >+ }, function(err, e){ >+ }, function(transaction){ >+ var items = transaction.objectStore("items"); >+ items.get(barcode).done(function (item, error) { >+ checkout(barcode, item, error); >+ }); >+ }); >+ }); >+}); >+//]]> >+</script> >+</head> >+<body id="circ_offline" class="circ"> >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'circ-search.inc' %] >+<div class="loading-overlay" style="display: none;"> >+ <div>Downloading records, please wait...</div> >+</div> >+ >+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a id="go-to-home" href="#offline-home">Offline circulation</a></div> >+ >+<div id="doc3" class="yui-t2"> >+ >+ <div id="bd"> >+ <div id="yui-main"> >+ <audio id="alert_sound" src="/intranet-tmpl/prog/sound/critical.ogg" autobuffer="autobuffer"></audio> >+ <audio id="success_sound" src="/intranet-tmpl/prog/sound/beep.ogg" autobuffer="autobuffer"></audio> >+ >+ <div id="alerts" class="yui-b"> >+ </div> >+ [% UNLESS (AllowOfflineCirculation) %] >+ <div id="noofflinecircwarning" class="dialog alert"> >+ <p><strong>Warning:</strong> Offline Circulation has been disabled. You may continue and record transactions, but patron and item information will not be available.</p> >+ </div> >+ [% END %] >+ >+ <div id="offline-home" class="yui-b offline-home"> >+ <div class="yui-g"> >+ <h1>Offline circulation</h1> >+ <div class="yui-u first"> >+ <ul> >+ <li><a id="go-to-circ" href="#offline-circulation">Check out</a></li> >+ <li><a id="go-to-returns" href="#offline-returns">Check in</a></li> >+ <li><a id="go-to-sync" href="#offline-sync">Synchronize (must be online)</a></li> >+ </ul> >+ </div> >+ >+ <div class="yui-u"> >+ <p><strong>Note:</strong> You must be online to use these options.</p> >+ <ul> >+ <li><a href="/cgi-bin/koha/offline_circ/list.pl">Pending offline circulation actions</a> >+ </ul> >+ </div> >+ </div> >+ </div> >+ >+ <div id="offline-sync" style="display: none;" class="yui-b offline-sync"> >+ <div id="toolbar" class="btn-toolbar"> >+ [% IF (AllowOfflineCirculation) %] >+ <a href="#" id="download-records" class="btn btn-small"><i class="icon-arrow-down"></i>Download records</a> >+ [% END %] >+ <a href="#" id="upload-transactions" class="btn btn-small"><i class="icon-arrow-up"></i>Upload transactions</a> >+ </div> >+ <div class="yui-g"> >+ <h1>Offline circulation</h1> >+ <div class="yui-u first"> >+ <div id="download-message"> >+ You have records in the offline circulation database on this >+ computer, but they may not be current: >+ <ul> >+ <li>Patron records were last synced on: <span id="patron-timestamp">(checking)</span></li> >+ <li>Item records were last synced on: <span id="item-timestamp">(checking)</span></li> >+ <li>Circulation records were last synced on: <span id="issue-timestamp">(checking)</span></li> >+ </ul> >+ </div> >+ </div> >+ >+ <div class="yui-u"> >+ <div id="upload-message">You have transactions in the offline >+ circulation database on this computer that have not been >+ uploaded. >+ </div> >+ </div> >+ </div> >+ </div> >+ >+ <div id="offline-returns" style="display: none;" class="yui-b offline-returns"> >+ <div class="yui-g"> >+ <form id="checkin-form" method="post" action="/cgi-bin/koha/circ/returns.pl" autocomplete="off" > >+ <div class="yui-u first"> >+ <fieldset> >+ <legend>Check In</legend> >+ <label for="barcode">Enter item barcode: </label> >+ <input name="barcode" id="barcode" size="14" class="focus"/> >+ <input type="submit" class="submit" value="Submit" /> >+ </fieldset> >+ </div> >+ </form> >+ </div> >+ >+ <div id="session-returned" style="display: none;"> >+ <h2>Checked-in items</h2> >+ <table id="already-checked-in"> >+ <thead> >+ <tr><th>Title</th><th>Author</th><th>Barcode</th><th>Home library</th><th>Holding library</th><th>Shelving location</th><th>Call number</th><th>Type</th></tr> >+ </thead> >+ <tbody> >+ </tbody> >+ </table> >+ </div> >+ </div> >+ >+ <div id="offline-circulation" style="display: none;" class="yui-b offline-circulation"> >+ <div class="yui-g"> >+ <form method="post" action="/cgi-bin/koha/circ/offline-circulation.pl" id="mainform" name="mainform" autocomplete="off"> >+ <fieldset id="circ_circulation_issue"> >+ <span id="clearscreen"><a href="/cgi-bin/koha/circ/offline-circulation.pl" title="Clear screen">x</a></span> >+ <label for="barcode">Checking out to <span class="patron-title"></span></label> >+ <div class="hint">Enter item barcode:</div> >+ <input type="text" name="barcode" id="barcode" class="barcode focus" size="14" /> >+ <input type="submit" value="Check Out" /> >+ >+ <div class="date-select"> >+ <div class="hint">Specify due date [% INCLUDE 'date-format.inc' %]: </div> >+ <input type="text" size="13" id="duedatespec" name="duedatespec" value="[% duedatespec %]" readonly="readonly" /> >+ <label for="stickyduedate"> Remember for session:</label> >+ <input type="checkbox" id="stickyduedate" onclick="this.form.barcode.focus();" name="stickyduedate" checked="checked" /> >+ <input type="button" class="action" id="cleardate" value="Clear" name="cleardate" onclick="this.checked = false; this.form.duedatespec.value = ''; this.form.stickyduedate.checked = false; this.form.barcode.focus(); return false;" /> >+ </div> >+ </fieldset> >+ </form> >+ </div> >+ >+ <div class="yui-g"><div id="patronlists" class="toptabs"> >+ <ul> >+ <li><a href="#checkouts"><span class="checkout-count">0</span> Checkouts</a></li> >+ <li><a href="#fines"><span class="fine-amount">0</span> in fines</a></li> >+ </ul> >+ >+ <!-- SUMMARY : TODAY & PREVIOUS ISSUES --> >+ <div id="checkouts"> >+ <div id="session-issues"> >+ <table id="issuest"> >+ <thead><tr> >+ <th scope="col">Due date</th> >+ <th scope="col">Title</th> >+ <th scope="col">Barcode</th> >+ <th scope="col">Item type</th> >+ <th scope="col">Checked out on</th> >+ <th scope="col">Checked out from</th> >+ <th scope="col">Call no</th> >+ <th scope="col">Charge</th> >+ </tr></thead> >+ <tbody> >+ </tbody> >+ </table> >+ </div> >+ >+ <div id="oldissues"> >+ <h5>Previous checkouts</h5> >+ <table id="oldissuest"> >+ <thead><tr> >+ <th scope="col">Due date</th> >+ <th scope="col">Title</th> >+ <th scope="col">Barcode</th> >+ <th scope="col">Item type</th> >+ <th scope="col">Checked out on</th> >+ <th scope="col">Checked out from</th> >+ <th scope="col">Call no</th> >+ <th scope="col">Charge</th> >+ </tr></thead> >+ <tbody> >+ </tbody> >+ </table> >+ </div> >+ </div> >+ >+ <div id="fines"> >+ <span class="patron-title"></span> has <span class="fine-amount">0</span> in fines. If you would like you can record payments. >+ <fieldset><legend>Pay fines</legend> >+ <label for="pay-fine-amount">Fine amount: </label><input type="text" name="pay-fine-amount" id="pay-fine-amount"/> >+ <button id="pay-fine" class="submit">Pay fine</button> >+ >+ <table id="session-payments" style="display: none;"> >+ <thead><tr><th>Amount</th><th>Timestamp</th></tr></thead> >+ <tbody></tbody> >+ </table> >+ </fieldset> >+ </div> >+ </div> >+ </div> >+ </div> >+ </div> >+ >+ <div class="yui-b offline-circulation" style="display: none;"> >+ <div class="patroninfo"><h5 class="patron-title"></h5> >+ <ul> >+ <li id="patron-address-1"></li> >+ <li id="patron-address-2"></li> >+ <li id="patron-address-parts"><!-- city, state, zipcode, country --></li> >+ <li id="patron-phone"></li> >+ <li id="patron-email"></li> >+ <li id="patron-category"></li> >+ <li id="patron-library"></li> >+ </ul> >+ </div> >+ >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/offline_circ/download.pl b/offline_circ/download.pl >new file mode 100755 >index 0000000..2b6b5af >--- /dev/null >+++ b/offline_circ/download.pl >@@ -0,0 +1,107 @@ >+#!/usr/bin/perl >+ >+# Copyright 2013 C & P Bibliography Services >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along with >+# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place, >+# Suite 330, Boston, MA 02111-1307 USA >+# >+ >+use Modern::Perl; >+use CGI; >+use JSON; >+use C4::Auth; >+use C4::Output; >+use C4::Context; >+use C4::Koha; >+ >+my $query = new CGI; >+my ( $template, $loggedinuser, $cookie, $flags ) = >+ checkauth( $query, undef, { circulate => "circulate_remaining_permissions" }, >+ "intranet" ); >+ >+my $page = $query->param('page') || 0; >+my $startrec = int($page) * 5000; >+my $req_data = $query->param('data') || ''; >+ >+my $patrons_query = qq{SELECT >+ borrowers.borrowernumber, cardnumber, surname, firstname, title, >+ othernames, initials, streetnumber, streettype, address, address2, city, >+ state, zipcode, country, email, phone, mobile, fax, dateofbirth, branchcode, >+ categorycode, dateenrolled, dateexpiry, gonenoaddress, lost, debarred, >+ debarredcomment, SUM(accountlines.amountoutstanding) AS fine >+ FROM borrowers >+ LEFT JOIN accountlines ON borrowers.borrowernumber=accountlines.borrowernumber >+ GROUP BY borrowers.borrowernumber >+ LIMIT $startrec, 5000; >+ }; >+ >+# NOTE: we can't fit very long titles on the interface so there isn't really any point in transferring them >+my $items_query = qq{SELECT >+ items.barcode AS barcode, items.itemnumber AS itemnumber, >+ items.itemcallnumber AS callnumber, items.homebranch AS homebranch, >+ items.holdingbranch AS holdingbranch, items.itype AS itemtype, >+ items.materials AS materials, LEFT(biblio.title, 60) AS title, >+ biblio.author AS author, biblio.biblionumber AS biblionumber >+ FROM items >+ JOIN biblio ON biblio.biblionumber = items.biblionumber >+ LIMIT $startrec, 5000; >+ }; >+ >+my $issues_query = qq{SELECT >+ biblio.title AS title, >+ items.barcode AS barcode, >+ items.itemcallnumber AS callnumber, >+ issues.date_due AS date_due, >+ issues.issuedate AS issuedate, >+ issues.renewals AS renewals, >+ borrowers.cardnumber AS cardnumber, >+ CONCAT(borrowers.surname, ', ', borrowers.firstname) AS borrower_name >+ FROM issues >+ JOIN items ON items.itemnumber = issues.itemnumber >+ JOIN biblio ON biblio.biblionumber = items.biblionumber >+ JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber >+ LIMIT $startrec, 5000; >+ }; >+ >+if ( $req_data eq 'all' ) { >+ print $query->header( -type => 'application/json', -charset => 'utf-8' ); >+ print to_json( >+ { >+ 'patrons' => get_data( $patrons_query, 'cardnumber' ), >+ 'items' => get_data( $items_query, 'barcode' ), >+ 'issues' => get_data( $issues_query, 'barcode' ), >+ } >+ ); >+} >+elsif ( $req_data eq 'patrons' ) { >+ print $query->header( -type => 'application/json', -charset => 'utf-8' ); >+ print to_json( { 'patrons' => get_data( $patrons_query, 'cardnumber' ), } ); >+} >+elsif ( $req_data eq 'items' ) { >+ print $query->header( -type => 'application/json', -charset => 'utf-8' ); >+ print to_json( { 'items' => get_data( $items_query, 'barcode' ), } ); >+} >+elsif ( $req_data eq 'issues' ) { >+ print $query->header( -type => 'application/json', -charset => 'utf-8' ); >+ print to_json( { 'issues' => get_data( $issues_query, 'barcode' ), } ); >+} >+ >+sub get_data { >+ my ( $sql, $key ) = @_; >+ my $dbh = C4::Context->dbh; >+ my $sth = $dbh->prepare($sql); >+ $sth->execute(); >+ return $sth->fetchall_hashref($key); >+} >diff --git a/offline_circ/service.pl b/offline_circ/service.pl >index 38f46d8..2dcc640 100755 >--- a/offline_circ/service.pl >+++ b/offline_circ/service.pl >@@ -29,6 +29,7 @@ my $cgi = CGI->new; > > # get the status of the user, this will check his credentials and rights > my ($status, $cookie, $sessionId) = C4::Auth::check_api_auth($cgi, undef); >+($status, $sessionId) = C4::Auth::check_cookie_auth($cgi, undef) if ($status ne 'ok'); > > my $result; > >-- >1.7.9.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 10240
:
18406
|
18448
|
19005
|
19065
|
19204
|
19328
|
19346
|
19347
|
19353
|
19528
|
20554
|
20555
|
20556
|
20762
|
20995
|
21157
|
21158
|
21159
|
21160
|
21161
|
21495
|
21496
|
21497
|
21498
|
21499
|
21500
|
21552
|
21553
|
21554
|
21555
|
21556
|
21557
|
21558
|
21559
|
21560
|
21573
|
21574
|
21575
|
21576
|
21577
|
21578
|
21579
|
21718
|
21719
|
21720
|
21721
|
21722
|
21723
|
21724
|
21725
|
21726