diff --git a/server/src/uds/REST/methods/users.py b/server/src/uds/REST/methods/users.py index 700d47e67..ab1cd047e 100644 --- a/server/src/uds/REST/methods/users.py +++ b/server/src/uds/REST/methods/users.py @@ -67,7 +67,7 @@ class Users(DetailHandler): def getTitle(self): try: - return _('Users of {0}').format(Authenticator.objects.get(pk=self._kwargs['parent_id'])) + return _('Users of {0}').format(Authenticator.objects.get(pk=self._kwargs['parent_id']).name) except: return _('Current users') diff --git a/server/src/uds/static/adm/js/Blob.js b/server/src/uds/static/adm/js/Blob.js new file mode 100644 index 000000000..5893afcab --- /dev/null +++ b/server/src/uds/static/adm/js/Blob.js @@ -0,0 +1,166 @@ +/* Blob.js + * A Blob implementation. + * 2013-06-20 + * + * By Eli Grey, http://eligrey.com + * By Devin Samarin, https://github.com/eboyjr + * License: X11/MIT + * See LICENSE.md + */ + +/*global self, unescape */ +/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, + plusplus: true */ + +/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ + +if (!(typeof Blob === "function" || typeof Blob === "object") || typeof URL === "undefined") +if ((typeof Blob === "function" || typeof Blob === "object") && typeof webkitURL !== "undefined") self.URL = webkitURL; +else var Blob = (function (view) { + "use strict"; + + var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) { + var + get_class = function(object) { + return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; + } + , FakeBlobBuilder = function BlobBuilder() { + this.data = []; + } + , FakeBlob = function Blob(data, type, encoding) { + this.data = data; + this.size = data.length; + this.type = type; + this.encoding = encoding; + } + , FBB_proto = FakeBlobBuilder.prototype + , FB_proto = FakeBlob.prototype + , FileReaderSync = view.FileReaderSync + , FileException = function(type) { + this.code = this[this.name = type]; + } + , file_ex_codes = ( + "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " + + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" + ).split(" ") + , file_ex_code = file_ex_codes.length + , real_URL = view.URL || view.webkitURL || view + , real_create_object_URL = real_URL.createObjectURL + , real_revoke_object_URL = real_URL.revokeObjectURL + , URL = real_URL + , btoa = view.btoa + , atob = view.atob + + , ArrayBuffer = view.ArrayBuffer + , Uint8Array = view.Uint8Array + ; + FakeBlob.fake = FB_proto.fake = true; + while (file_ex_code--) { + FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; + } + if (!real_URL.createObjectURL) { + URL = view.URL = {}; + } + URL.createObjectURL = function(blob) { + var + type = blob.type + , data_URI_header + ; + if (type === null) { + type = "application/octet-stream"; + } + if (blob instanceof FakeBlob) { + data_URI_header = "data:" + type; + if (blob.encoding === "base64") { + return data_URI_header + ";base64," + blob.data; + } else if (blob.encoding === "URI") { + return data_URI_header + "," + decodeURIComponent(blob.data); + } if (btoa) { + return data_URI_header + ";base64," + btoa(blob.data); + } else { + return data_URI_header + "," + encodeURIComponent(blob.data); + } + } else if (real_create_object_URL) { + return real_create_object_URL.call(real_URL, blob); + } + }; + URL.revokeObjectURL = function(object_URL) { + if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { + real_revoke_object_URL.call(real_URL, object_URL); + } + }; + FBB_proto.append = function(data/*, endings*/) { + var bb = this.data; + // decode data to a binary string + if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { + var + str = "" + , buf = new Uint8Array(data) + , i = 0 + , buf_len = buf.length + ; + for (; i < buf_len; i++) { + str += String.fromCharCode(buf[i]); + } + bb.push(str); + } else if (get_class(data) === "Blob" || get_class(data) === "File") { + if (FileReaderSync) { + var fr = new FileReaderSync; + bb.push(fr.readAsBinaryString(data)); + } else { + // async FileReader won't work as BlobBuilder is sync + throw new FileException("NOT_READABLE_ERR"); + } + } else if (data instanceof FakeBlob) { + if (data.encoding === "base64" && atob) { + bb.push(atob(data.data)); + } else if (data.encoding === "URI") { + bb.push(decodeURIComponent(data.data)); + } else if (data.encoding === "raw") { + bb.push(data.data); + } + } else { + if (typeof data !== "string") { + data += ""; // convert unsupported types to strings + } + // decode UTF-16 to binary string + bb.push(unescape(encodeURIComponent(data))); + } + }; + FBB_proto.getBlob = function(type) { + if (!arguments.length) { + type = null; + } + return new FakeBlob(this.data.join(""), type, "raw"); + }; + FBB_proto.toString = function() { + return "[object BlobBuilder]"; + }; + FB_proto.slice = function(start, end, type) { + var args = arguments.length; + if (args < 3) { + type = null; + } + return new FakeBlob( + this.data.slice(start, args > 1 ? end : this.data.length) + , type + , this.encoding + ); + }; + FB_proto.toString = function() { + return "[object Blob]"; + }; + return FakeBlobBuilder; + }(view)); + + return function Blob(blobParts, options) { + var type = options ? (options.type || "") : ""; + var builder = new BlobBuilder(); + if (blobParts) { + for (var i = 0, len = blobParts.length; i < len; i++) { + builder.append(blobParts[i]); + } + } + return builder.getBlob(type); + }; +}(self)); diff --git a/server/src/uds/static/adm/js/FileSaver.js b/server/src/uds/static/adm/js/FileSaver.js new file mode 100644 index 000000000..378a9dcc4 --- /dev/null +++ b/server/src/uds/static/adm/js/FileSaver.js @@ -0,0 +1,232 @@ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 2013-10-21 + * + * By Eli Grey, http://eligrey.com + * License: X11/MIT + * See LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, + plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs + || (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) + || (function(view) { + "use strict"; + var + doc = view.document + // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , URL = view.URL || view.webkitURL || view + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = !view.externalHost && "download" in save_link + , click = function(node) { + var event = doc.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, false, view, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + node.dispatchEvent(event); + } + , webkit_req_fs = view.webkitRequestFileSystem + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem + , throw_outside = function (ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + , fs_min_size = 0 + , deletion_queue = [] + , process_deletion_queue = function() { + var i = deletion_queue.length; + while (i--) { + var file = deletion_queue[i]; + if (typeof file === "string") { // file is an object URL + URL.revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + } + deletion_queue.length = 0; // clear queue + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , FileSaver = function(blob, name) { + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , blob_changed = false + , object_url + , target_view + , get_object_url = function() { + var object_url = get_URL().createObjectURL(blob); + deletion_queue.push(object_url); + return object_url; + } + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + // don't create more object URLs than needed + if (blob_changed || !object_url) { + object_url = get_object_url(blob); + } + if (target_view) { + target_view.location.href = object_url; + } else { + window.open(object_url, "_blank"); + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + } + , abortable = function(func) { + return function() { + if (filesaver.readyState !== filesaver.DONE) { + return func.apply(this, arguments); + } + }; + } + , create_if_not_found = {create: true, exclusive: false} + , slice + ; + filesaver.readyState = filesaver.INIT; + if (!name) { + name = "download"; + } + if (can_use_save_link) { + object_url = get_object_url(blob); + // FF for Android has a nasty garbage collection mechanism + // that turns all objects that are not pure javascript into 'deadObject' + // this means `doc` and `save_link` are unusable and need to be recreated + // `view` is usable though: + doc = view.document; + save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"); + save_link.href = object_url; + save_link.download = name; + var event = doc.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, false, view, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + save_link.dispatchEvent(event); + filesaver.readyState = filesaver.DONE; + dispatch_all(); + return; + } + // Object and web filesystem URLs have a problem saving in Google Chrome when + // viewed in a tab, so I force save with application/octet-stream + // http://code.google.com/p/chromium/issues/detail?id=91158 + if (view.chrome && type && type !== force_saveable_type) { + slice = blob.slice || blob.webkitSlice; + blob = slice.call(blob, 0, blob.size, force_saveable_type); + blob_changed = true; + } + // Since I can't be sure that the guessed media type will trigger a download + // in WebKit, I append .download to the filename. + // https://bugs.webkit.org/show_bug.cgi?id=65440 + if (webkit_req_fs && name !== "download") { + name += ".download"; + } + if (type === force_saveable_type || webkit_req_fs) { + target_view = view; + } + if (!req_fs) { + fs_error(); + return; + } + fs_min_size += blob.size; + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { + var save = function() { + dir.getFile(name, create_if_not_found, abortable(function(file) { + file.createWriter(abortable(function(writer) { + writer.onwriteend = function(event) { + target_view.location.href = file.toURL(); + deletion_queue.push(file); + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "writeend", event); + }; + writer.onerror = function() { + var error = writer.error; + if (error.code !== error.ABORT_ERR) { + fs_error(); + } + }; + "writestart progress write abort".split(" ").forEach(function(event) { + writer["on" + event] = filesaver["on" + event]; + }); + writer.write(blob); + filesaver.abort = function() { + writer.abort(); + filesaver.readyState = filesaver.DONE; + }; + filesaver.readyState = filesaver.WRITING; + }), fs_error); + }), fs_error); + }; + dir.getFile(name, {create: false}, abortable(function(file) { + // delete file if it already exists + file.remove(); + save(); + }), abortable(function(ex) { + if (ex.code === ex.NOT_FOUND_ERR) { + save(); + } else { + fs_error(); + } + })); + }), fs_error); + }), fs_error); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name) { + return new FileSaver(blob, name); + } + ; + FS_proto.abort = function() { + var filesaver = this; + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "abort"); + }; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + view.addEventListener("unload", process_deletion_queue, false); + return saveAs; +}(this.self || this.window || this.content)); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== 'undefined') module.exports = saveAs; diff --git a/server/src/uds/static/adm/js/api.js b/server/src/uds/static/adm/js/api.js index 7f43691f1..ee9c30d8b 100644 --- a/server/src/uds/static/adm/js/api.js +++ b/server/src/uds/static/adm/js/api.js @@ -41,92 +41,89 @@ // We will take advantage of this and save a lot of nonsense, prone to failure // code :-) -function BasicModelRest(path) { - this.path = path || ""; - this.cached_types = undefined; - this.cached_tableInfo = undefined; +function BasicModelRest(path, options) { + options = options || {}; + path = path || ''; + // Requests paths + this.path = path; + this.getPath = options.getPath || path; + this.typesPath = options.typesPath || (path + '/types'); + this.tableInfoPath = options.tableInfoPath || (path + '/tableinfo'); + this.cache = api.cache('bmr'+path); } BasicModelRest.prototype = { - get : function(options, alternate_url) { - if (options === undefined) { - options = {}; - } - var path = alternate_url || this.path; + get : function(options) { + options = options || {}; + + var path = this.getPath; if (options.id !== undefined) path += '/' + options.id; api.getJson(path, options.success); }, - types : function(success_fnc, alternate_url) { + types : function(success_fnc) { // Cache types locally, will not change unless new broker version - if (this.cached_types) { - if (success_fnc) { - success_fnc(this.cached_types); - } + sucess_fnc = success_fnc || function(data){}; + if( this.typesPath == '.' ) { + success_fnc({}); + return; + } + if (this.cache.get('types')) { + success_fnc(this.cache.get('types')); } else { var $this = this; - var path = this.path + '/types'; - if (alternate_url !== undefined) - path = alternate_url; + var path = this.typesPath; api.getJson(path, function(data) { - $this.cached_types = data; - if (success_fnc) { - success_fnc($this.cached_types); - } + $this.cache.put('types', data); + success_fnc(data); }); } }, - tableInfo : function(success_fnc, alternate_url) { + tableInfo : function(success_fnc) { + var path = this.tableInfoPath; // Cache types locally, will not change unless new broker version - if (this.cached_tableInfo) { + if( this.cache.get(path) ) { if (success_fnc) { - success_fnc(this.cached_tableInfo); + success_fnc(this.cache.get(path)); } return; } - var $this = this; - var path = this.path + '/tableinfo'; - if (alternate_url !== undefined) - path = alternate_url; + var $this = this; api.getJson(path, function(data) { - $this.cached_tableInfo = data; - if (success_fnc) { - success_fnc($this.cached_tableInfo); - } + $this.cache.put(path, data); + success_fnc(data); }); }, + + detail: function(id, child) { + return new DetailModelRestApi(this, id, child); + } }; // For REST of type /auth/[id]/users, /services/[id]/users, ... -function DetailModelRestApi(parentApi, path) { - this.parentPath = parentApi.path; - this.path = path; +function DetailModelRestApi(parentApi, parentId, model) { + this.base = new BasicModelRest(undefined, { + getPath: [parentApi.path, parentId, model].join('/'), + typesPath: '.', // We do not has this on details + tableInfoPath: [parentApi.path, 'tableinfo', parentId, model].join('/'), + }); } DetailModelRestApi.prototype = { // Generates a basic model with fixed methods for "detail" models - detail : function(parentId) { - var $this = this; - var rest = new BasicModelRest(this.parentPath + '/' + parentId + '/' + this.path); - - // Overwrite types, detail do not have types - rest.types = function() { - return []; // No types at all - }; - - // And overwrite tableInfo - var parentTableInfo = rest.tableInfo; - rest.tableInfo = function(success_fnc, alternate_url) { - if (alternate_url === undefined) - alternate_url = $this.parentPath + '/tableinfo/' + parentId + '/' + $this.path; - parentTableInfo(success_fnc, alternate_url); - }; - return rest; - } + get: function(options) { + return this.base.get(options); + }, + types: function(success_fnc) { + return this.base.types(success_fnc); + }, + tableInfo: function(success_fnc) { + return this.base.tableInfo(success_fnc); + }, }; // Populate api @@ -134,7 +131,6 @@ DetailModelRestApi.prototype = { api.providers = new BasicModelRest('providers'); // api.services = new BasicModelRest('services'); api.authenticators = new BasicModelRest('authenticators'); -api.authenticators.users = new DetailModelRestApi(api.authenticators, 'users'); api.osmanagers = new BasicModelRest('osmanagers'); api.transports = new BasicModelRest('transports'); diff --git a/server/src/uds/static/adm/js/cache.js b/server/src/uds/static/adm/js/cache.js new file mode 100644 index 000000000..d45d803ee --- /dev/null +++ b/server/src/uds/static/adm/js/cache.js @@ -0,0 +1,31 @@ +(function(api, $, undefined) { + + api.cache = function(cacheName) { + return new Cache(cacheName); + }; + +}(window.api = window.api || {}, jQuery)); + +function Cache(cacheName) { + api.cacheTable = api.cacheTable || {}; + + api.cacheTable[cacheName] = api.cacheTable[cacheName] || {}; + + this.name = cacheName; + this.cache = api.cacheTable[cacheName]; +} + +Cache.prototype = { + get: function(key, not_found_fnc){ + not_found_fnc = not_found_fnc || function() { return undefined; }; + + if( this.cache[key] === undefined ) { + this.cache[key] = not_found_fnc(); + } + return this.cache[key]; + }, + + put: function(key, value) { + this.cache[key] = value; + }, +}; \ No newline at end of file diff --git a/server/src/uds/static/adm/js/dataTables.bootstrap.js b/server/src/uds/static/adm/js/dataTables.bootstrap.js index b49650e72..2db25c528 100644 --- a/server/src/uds/static/adm/js/dataTables.bootstrap.js +++ b/server/src/uds/static/adm/js/dataTables.bootstrap.js @@ -1,148 +1,144 @@ /* Set the defaults for DataTables initialisation */ -$.extend( true, $.fn.dataTable.defaults, { - "sDom": "<'row'<'col-xs-6'l><'col-xs-6'f>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>", - "sPaginationType": "bootstrap", - "oLanguage": { - "sLengthMenu": "_MENU_ records per page" - } -} ); +$.extend(true, $.fn.dataTable.defaults, { + "sDom" : "<'row'<'col-xs-6'l><'col-xs-6'f>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>", + "sPaginationType" : "bootstrap", + "oLanguage" : { + "sLengthMenu" : "_MENU_ records per page" + } +}); /* Default class modification */ -$.extend( $.fn.dataTableExt.oStdClasses, { - "sWrapper": "dataTables_wrapper form-inline", - "sFilterInput": "form-control input-sm", - "sLengthSelect": "form-control input-sm" -} ); +$.extend($.fn.dataTableExt.oStdClasses, { + "sWrapper" : "dataTables_wrapper form-inline", + "sFilterInput" : "form-control input-sm", + "sLengthSelect" : "form-control input-sm" +}); /* API method to get paging information */ -$.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings ) -{ - return { - "iStart": oSettings._iDisplayStart, - "iEnd": oSettings.fnDisplayEnd(), - "iLength": oSettings._iDisplayLength, - "iTotal": oSettings.fnRecordsTotal(), - "iFilteredTotal": oSettings.fnRecordsDisplay(), - "iPage": oSettings._iDisplayLength === -1 ? - 0 : Math.ceil( oSettings._iDisplayStart / oSettings._iDisplayLength ), - "iTotalPages": oSettings._iDisplayLength === -1 ? - 0 : Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength ) - }; +$.fn.dataTableExt.oApi.fnPagingInfo = function(oSettings) { + return { + "iStart" : oSettings._iDisplayStart, + "iEnd" : oSettings.fnDisplayEnd(), + "iLength" : oSettings._iDisplayLength, + "iTotal" : oSettings.fnRecordsTotal(), + "iFilteredTotal" : oSettings.fnRecordsDisplay(), + "iPage" : oSettings._iDisplayLength === -1 ? 0 : Math + .ceil(oSettings._iDisplayStart / oSettings._iDisplayLength), + "iTotalPages" : oSettings._iDisplayLength === -1 ? 0 : Math.ceil(oSettings.fnRecordsDisplay() / + oSettings._iDisplayLength) + }; }; - /* Bootstrap style pagination control */ -$.extend( $.fn.dataTableExt.oPagination, { - "bootstrap": { - "fnInit": function( oSettings, nPaging, fnDraw ) { - var oLang = oSettings.oLanguage.oPaginate; - var fnClickHandler = function ( e ) { - e.preventDefault(); - if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) { - fnDraw( oSettings ); - } - }; +$.extend($.fn.dataTableExt.oPagination, { + "bootstrap" : { + "fnInit" : function(oSettings, nPaging, fnDraw) { + var oLang = oSettings.oLanguage.oPaginate; + var fnClickHandler = function(e) { + e.preventDefault(); + if (oSettings.oApi._fnPageChange(oSettings, e.data.action)) { + fnDraw(oSettings); + } + }; - $(nPaging).append( - '