diff --git a/server/src/uds/REST/__init__.py b/server/src/uds/REST/__init__.py index 3b8c90117..e0fda0421 100644 --- a/server/src/uds/REST/__init__.py +++ b/server/src/uds/REST/__init__.py @@ -37,7 +37,7 @@ from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _, activate from django.conf import settings -from handlers import Handler, HandlerError, AccessDenied, NotFound +from handlers import Handler, HandlerError, AccessDenied, NotFound, RequestError import time import logging @@ -148,12 +148,14 @@ class Dispatcher(View): for k, v in handler.headers().iteritems(): response[k] = v return response - except HandlerError as e: - return http.HttpResponseBadRequest(unicode(e)) + except RequestError as e: + return http.HttpResponseServerError(unicode(e)) except AccessDenied as e: return http.HttpResponseForbidden(unicode(e)) except NotFound as e: return http.Http404(unicode(e)) + except HandlerError as e: + return http.HttpResponseBadRequest(unicode(e)) except Exception as e: logger.exception('Error processing request') return http.HttpResponseServerError(unicode(e)) diff --git a/server/src/uds/REST/handlers.py b/server/src/uds/REST/handlers.py index 58c4314b9..26e14cb22 100644 --- a/server/src/uds/REST/handlers.py +++ b/server/src/uds/REST/handlers.py @@ -32,7 +32,6 @@ ''' from __future__ import unicode_literals from django.contrib.sessions.backends.db import SessionStore -from django.utils.translation import activate from django.conf import settings from uds.core.util.Config import GlobalConfig @@ -52,6 +51,9 @@ class NotFound(HandlerError): class AccessDenied(HandlerError): pass +class RequestError(HandlerError): + pass + class Handler(object): raw = False # If true, Handler will return directly an HttpResponse Object name = None # If name is not used, name will be the class name in lower case diff --git a/server/src/uds/REST/methods/transports.py b/server/src/uds/REST/methods/transports.py index 4c61e008b..b1394e230 100644 --- a/server/src/uds/REST/methods/transports.py +++ b/server/src/uds/REST/methods/transports.py @@ -32,7 +32,7 @@ ''' from __future__ import unicode_literals -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ugettext from uds.models import Transport from uds.core.transports import factory @@ -47,6 +47,7 @@ logger = logging.getLogger(__name__) class Transports(ModelHandlerMixin, Handler): model = Transport + save_fields = ['name', 'comments', 'priority', 'nets_positive'] def item_as_dict(self, item): type_ = item.getType() @@ -58,16 +59,27 @@ class Transports(ModelHandlerMixin, Handler): 'deployed_count': item.deployedServices.count(), 'type': type_.type(), } + + class Types(ModelTypeHandlerMixin, Handler): path = 'transports' + has_comments = True def enum_types(self): return factory().providers().values() def getGui(self, type_): try: - return factory().lookup(type_).guiDescription() + return self.addField(self.addDefaultFields(factory().lookup(type_).guiDescription(), ['name', 'comments']), { + 'name': 'priority', + 'required': True, + 'value': '1', + 'label': ugettext('Priority'), + 'tooltip': ugettext('Priority of this transport'), + 'type': 'numeric', + 'order': 100, # At end + }) except: raise NotFound('type not found') @@ -78,5 +90,6 @@ class TableInfo(ModelTableHandlerMixin, Handler): fields = [ { 'name': {'title': _('Name'), 'visible': True, 'type': 'iconType' } }, { 'comments': {'title': _('Comments')}}, + { 'priority': {'title': _('Priority'), 'type': 'numeric', 'width': '6em' }}, { 'deployed_count': {'title': _('Used by'), 'type': 'numeric', 'width': '8em'}} ] diff --git a/server/src/uds/REST/mixins.py b/server/src/uds/REST/mixins.py index dbd9e7e13..5db4d3078 100644 --- a/server/src/uds/REST/mixins.py +++ b/server/src/uds/REST/mixins.py @@ -32,7 +32,7 @@ ''' from __future__ import unicode_literals -from handlers import NotFound +from handlers import NotFound, RequestError from django.utils.translation import ugettext as _ import logging @@ -65,6 +65,8 @@ class ModelHandlerMixin(object): needs_staff = True detail = None # Dictionary containing detail routing model = None + save_fields = [] + def item_as_dict(self, item): pass @@ -117,6 +119,37 @@ class ModelHandlerMixin(object): return res except: raise NotFound('item not found') + + def put(self): + logger.debug('method PUT for {0}, {1}'.format(self.__class__.__name__, self._args)) + args = {} + try: + for key in self.save_fields: + args[key] = self._params[key] + del self._params[key] + except KeyError as e: + raise RequestError('needed parameter not found in data {0}'.format(unicode(e))) + try: + item = self.model.objects.create(**args); + except: # Duplicate key probably + raise RequestError('Element already exists (duplicate key error)') + + try: + if self._params.has_key('data_type'): # Needs to store instance + item.data_type = self._params['data_type'] + item.data = item.getInstance(self._params).serialize() + item.save() + except Exception as e: + item.delete() # Remove pre-saved element + raise RequestError(unicode(e)) + + return {'id': item.id } + + def delete(self): + logger.debug('method DELETE for {0}, {1}'.format(self.__class__.__name__, self._args)) + if len(self._args) != 1: + raise RequestError('Delete need an argument') + return 'deleted' class ModelTypeHandlerMixin(object): ''' @@ -125,6 +158,7 @@ class ModelTypeHandlerMixin(object): ''' authenticated = True needs_staff = True + has_comments = False def enum_types(self): pass @@ -140,6 +174,46 @@ class ModelTypeHandlerMixin(object): for type_ in self.enum_types(): yield self.type_as_dict(type_) + def addField(self, gui, field): + gui.append({ + 'name': field.get('name', ''), + 'value': '', + 'gui': { + 'required': field.get('required', False), + 'defvalue': field.get('value', ''), + 'value': field.get('value', ''), + 'label': field.get('label', ''), + 'length': field.get('length', 128), + 'multiline': field.get('multiline', 0), + 'tooltip': field.get('tooltip', ''), + 'rdonly': field.get('rdonly', False), + 'type': field.get('type', 'text'), + 'order': field.get('order', 0), + 'values': field.get('values', []) + } + }) + return gui + + def addDefaultFields(self, gui, flds): + if 'name' in flds: + self.addField(gui, { + 'name': 'name', + 'required': True, + 'label': _('Name'), + 'tooltip': _('Name of this element'), + 'order': -2, + }) + # And maybe comments (only if model has this field) + if 'comments' in flds: + self.addField(gui, { + 'name': 'comments', + 'label': _('Comments'), + 'tooltip': _('Comments for this element'), + 'length': 256, + 'order': -1, + }) + return gui + def get(self): logger.debug(self._args) nArgs = len(self._args) @@ -162,42 +236,7 @@ class ModelTypeHandlerMixin(object): if self._args[1] == 'gui': gui = self.getGui(self._args[0]) # Add name default description, at top of form - gui.append({ - 'name': 'name', - 'value':'', - 'gui': { - 'required':True, - 'defvalue':'', - 'value':'', - 'label': _('Name'), - 'length': 128, - 'multiline': 0, - 'tooltip': _('Name of this element'), - 'rdonly': False, - 'type': 'text', - 'order': -2 - } - }) - # And comments - gui.append({ - 'name': 'comments', - 'value':'', - 'gui': { - 'required':False, - 'defvalue':'', - 'value':'', - 'label': _('Comments'), - 'length': 256, - 'multiline': 0, - 'tooltip': _('Comments for this element'), - 'rdonly': False, - 'type': 'text', - 'order': -1 - } - }) - - logger.debug("GUI: {0}".format(gui)) return sorted(gui, key=lambda f: f['gui']['order']); diff --git a/server/src/uds/core/ui/UserInterface.py b/server/src/uds/core/ui/UserInterface.py index 9dde74292..08020ffa2 100644 --- a/server/src/uds/core/ui/UserInterface.py +++ b/server/src/uds/core/ui/UserInterface.py @@ -125,7 +125,9 @@ class gui(object): Returns: True if the string is "true" (case insensitive), False else. ''' - if str_.lower() == gui.TRUE: + if isinstance(str_, bool): + return str_ + if unicode(str_).lower() == gui.TRUE: return True return False diff --git a/server/src/uds/static/adm/js/api.js b/server/src/uds/static/adm/js/api.js index 13e15efed..90c1b8048 100644 --- a/server/src/uds/static/adm/js/api.js +++ b/server/src/uds/static/adm/js/api.js @@ -40,9 +40,15 @@ } }; + // Default fail function + api.defaultFail = function(jqXHR, textStatus, errorThrown) { + api.doLog(jqXHR, ', ', textStatus, ', ', errorThrown); + }; + api.getJson = function(path, options) { options = options || {}; - var success_fnc = options.success || function(){}; + var success_fnc = options.success || function(){}; + var fail_fnc = options.fail || api.defaultFail; var url = api.url_for(path); api.doLog('Ajax GET Json for "' + url + '"'); @@ -51,10 +57,41 @@ type : "GET", dataType : "json", success : function(data) { - api.doLog('Success on "' + url + '".'); - api.doLog('Received ' + JSON.stringify(data)); + api.doLog('Success on GET "' + url + '".'); + api.doLog('Received ', data); success_fnc(data); }, + error: function(jqXHR, textStatus, errorThrown) { + api.doLog('Error on GET "' + url + '". ', textStatus, ', ', errorThrown); + fail_fnc(jqXHR, textStatus, errorThrown); + }, + beforeSend : function(request) { + request.setRequestHeader(api.config.auth_header, api.config.token); + }, + }); + }; + + api.putJson = function(path, data, options) { + options = options || {}; + var success_fnc = options.success || function(){}; + var fail_fnc = options.fail || api.defaultFail; + + var url = api.url_for(path); + api.doLog('Ajax PUT Json for "' + url + '"'); + $.ajax({ + url : url, + type : "PUT", + dataType : "json", + data: JSON.stringify(data), + success: function(data) { + api.doLog('Success on PUT "' + url + '".'); + api.doLog('Received ', data); + success_fnc(data); + }, + error: function(jqXHR, textStatus, errorThrown) { + api.doLog('Error on PUT "' + url + '". ', textStatus, ', ', errorThrown); + fail_fnc(jqXHR, textStatus, errorThrown); + }, beforeSend : function(request) { request.setRequestHeader(api.config.auth_header, api.config.token); }, @@ -62,7 +99,7 @@ }; // Public attributes - api.debug = false; + api.debug = true; }(window.api = window.api || {}, jQuery)); @@ -104,6 +141,7 @@ function BasicModelRest(path, options) { // Requests paths this.path = path; this.getPath = options.getPath || path; + this.putPath = options.putPath || path; this.typesPath = options.typesPath || (path + '/types'); this.tableInfoPath = options.tableInfoPath || (path + '/tableinfo'); this.cache = api.cache('bmr'+path); @@ -117,7 +155,8 @@ BasicModelRest.prototype = { _requestPath: function(path, options) { "use strict"; options = options || {}; - var success_fnc = options.success || function(){api.doLog('success not provided for '+path);}; + var success_fnc = options.success || function(){api.doLog('success function not provided for '+path);}; + var fail_fnc = options.fail; var cacheKey = options.cacheKey || path; if( path == '.' ) { @@ -136,10 +175,11 @@ BasicModelRest.prototype = { } success_fnc(data); }, + fail: fail_fnc, }); } }, - get : function(success_fnc, options) { + get: function(options) { "use strict"; options = options || {}; @@ -148,61 +188,103 @@ BasicModelRest.prototype = { path += '/' + options.id; return this._requestPath(path, { cacheKey: '.', // Right now, do not cache any "get" method - success: success_fnc, + success: options.success, + fail: options.fail }); }, - list: function(success_fnc, options) { // This is "almost" an alias for get + list: function(success_fnc, fail_fnc) { // This is "almost" an alias for get "use strict"; - options = options || {}; - return this.get(success_fnc, { + return this.get({ id: '', + success: success_fnc, + fail: fail_fnc }); }, - overview: function(success_fnc, options) { + overview: function(success_fnc, fail_fnc) { "use strict"; - options = options || {}; - return this.get(success_fnc, { + return this.get({ id: 'overview', + success: success_fnc, + fail: fail_fnc }); }, - item: function(itemId, success_fnc, options) { + item: function(itemId, success_fnc, fail_fnc) { "use strict"; - options = options || {}; - return this.get(success_fnc, { + return this.get({ id: itemId, + success: success_fnc, + fail: fail_fnc }); }, - types : function(success_fnc, options) { + + // ------------- + // Put methods + // ------------- + + put: function(data, options) { "use strict"; options = options || {}; + + var path = this.putPath; + if ( options.id ) + path += '/' + options.id; + + api.putJson(path, data, { + success: options.success, + fail: options.fail + }); + }, + create: function(data, success_fnc, fail_fnc) { + "use strict"; + + return this.put(data, { + success: success_fnc, + fail: fail_fnc + }); + }, + save: function(data, success_fnc, fail_fnc) { + "use strict"; + + return this.put(data, { + id: data.id, + success: success_fnc, + fail: fail_fnc + }); + }, + + // -------------- + // Types methods + // -------------- + types : function(success_fnc, fail_fnc) { + "use strict"; return this._requestPath(this.typesPath, { cacheKey: 'type', success: success_fnc, }); }, - gui: function(typeName, success_fnc, options) { + gui: function(typeName, success_fnc, fail_fnc) { // GUI returns a dict, that contains: // name: Name of the field // value: value of the field (selected element in choice, text for inputs, etc....) // gui: Description of the field (type, value or values, defvalue, .... "use strict"; - options = options || {}; var path = [this.typesPath, typeName, 'gui'].join('/'); return this._requestPath(path, { cacheKey: typeName + '-gui', success: success_fnc, + fail: fail_fnc, }); }, - tableInfo : function(success_fnc, options) { + tableInfo : function(success_fnc, fail_fnc) { "use strict"; - options = options || {}; success_fnc = success_fnc || function(){api.doLog('success not provided for tableInfo');}; var path = this.tableInfoPath; this._requestPath(path, { success: success_fnc, + fail: fail_fnc, }); }, diff --git a/server/src/uds/static/adm/js/gui-elements.js b/server/src/uds/static/adm/js/gui-definition.js similarity index 89% rename from server/src/uds/static/adm/js/gui-elements.js rename to server/src/uds/static/adm/js/gui-definition.js index bb027ac22..23a4492b6 100644 --- a/server/src/uds/static/adm/js/gui-elements.js +++ b/server/src/uds/static/adm/js/gui-definition.js @@ -153,12 +153,20 @@ gui.connectivity.link = function(event) { }); }); }, - onNew: function(type) { + onNew: function(type, table, refreshFnc) { gui.connectivity.transports.rest.gui(type, function(itemGui) { var form = gui.fields(itemGui); - gui.launchModalForm(gettext('New transport'), form, function(form_selector) { + gui.launchModalForm(gettext('New transport'), form, function(form_selector, closeFnc) { var fields = gui.fields.read(form_selector); - return false; + // Append "own" fields, in this case data_type + fields.data_type = type; + fields.nets_positive = false; + gui.connectivity.transports.rest.create(fields, function(data) { // Success on put + closeFnc(); + refreshFnc(); + }, function(jqXHR, textStatus, errorThrown) { // fail on put + gui.launchModal(gettext('Error creating transport'), jqXHR.responseText, ' '); + }); }); }); }, diff --git a/server/src/uds/static/adm/js/gui-element.js b/server/src/uds/static/adm/js/gui-element.js new file mode 100644 index 000000000..7f598047c --- /dev/null +++ b/server/src/uds/static/adm/js/gui-element.js @@ -0,0 +1,428 @@ +/* jshint strict: true */ +function BasicGuiElement(name) { + "use strict"; + this.name = name; +} + +function GuiElement(restItem, name) { + "use strict"; + this.rest = restItem; + this.name = name; + this.types = {}; + this.init(); +} + +// all gui elements has, at least, name && type +// Types must include, at least: type, icon +GuiElement.prototype = { + init : function() { + "use strict"; + gui.doLog('Initializing ' + this.name); + var $this = this; + this.rest.types(function(data) { + var styles = ''; + $.each(data, function(index, value) { + var className = $this.name + '-' + value.type; + $this.types[value.type] = { + css : className, + name : value.name || '', + description : value.description || '' + }; + gui.doLog('Creating style for ' + className); + var style = '.' + className + ' { display:inline-block; background: url(data:image/png;base64,' + + value.icon + '); ' + 'width: 16px; height: 16px; vertical-align: middle; } '; + styles += style; + }); + if (styles !== '') { + styles = ''; + $(styles).appendTo('head'); + } + }); + }, + // Options: dictionary + // container: container ID of parent for the table. If undefined, table will be appended to workspace + // buttons: array of visible buttons (strings), valid are [ 'new', 'edit', 'refresh', 'delete', 'xls' ], + // rowSelect: type of allowed row selection, valid values are 'single' and 'multi' + // scrollToTable: if True, will scroll page to show table + // + // onLoad: Event (function). If defined, will be invoked when table is fully loaded. + // Receives 1 parameter, that is the gui element (GuiElement) used to render table + // onRowSelect: Event (function). If defined, will be invoked when a row of table is selected + // Receives 3 parameters: + // 1.- the array of selected items data (objects, as got from api...get) + // 2.- the DataTable that raised the event + // 3.- the DataTableTools that raised the event + // onRowDeselect: Event (function). If defined, will be invoked when a row of table is deselected + // Receives 3 parameters: + // 1.- the array of selected items data (objects, as got from api...get) + // 2.- the DataTable that raised the event + // onNew: Event (function). If defined, will be invoked when "new" button is pressed + // Receives 4 parameters: + // 1.- the selected item data (single object, as got from api...get) + // 2.- the event that fired this (new, delete, edit, ..) + // 3.- the DataTable that raised the event + // onEdit: Event (function). If defined, will be invoked when "edit" button is pressed + // Receives 4 parameters: + // 1.- the selected item data (single object, as got from api...get) + // 2.- the event that fired this (new, delete, edit, ..) + // 3.- the DataTable that raised the event + // onDelete: Event (function). If defined, will be invoked when "delete" button is pressed + // Receives 4 parameters: + // 1.- the selected item data (single object, as got from api...get) + // 2.- the event that fired this (new, delete, edit, ..) + // 4.- the DataTable that raised the event + table : function(options) { + "use strict"; + gui.doLog('Types: ', this.types); + options = options || {}; + gui.doLog('Composing table for ' + this.name); + var tableId = this.name + '-table'; + var $this = this; // Store this for child functions + + // Empty cells transform + var renderEmptyCell = function(data) { + if( data === '' ) + return '-'; + return data; + }; + + // Datetime renderer (with specified format) + var renderDate = function(format) { + return function(data, type, full) { + return api.tools.strftime(format, new Date(data*1000)); + }; + }; + + // Icon renderer, based on type (created on init methods in styles) + var renderTypeIcon = function(data, type, value){ + if( type == 'display' ) { + var css = $this.types[value.type].css; + return ' ' + renderEmptyCell(data); + } else { + return renderEmptyCell(data); + } + }; + + // Custom icon renderer, in fact span with defined class + var renderIcon = function(icon) { + return function(data, type, full) { + if( type == 'display' ) { + return ' ' + renderEmptyCell(data); + } else { + return renderEmptyCell(data); + } + }; + }; + // Text transformation, dictionary based + var renderTextTransform = function(dict) { + return function(data, type, full) { + return dict[data] || renderEmptyCell(''); + }; + }; + this.rest.tableInfo(function(data) { + var title = data.title; + var columns = []; + $.each(data.fields, function(index, value) { + for ( var v in value) { + var opts = value[v]; + var column = { + mData : v, + }; + column.sTitle = opts.title; + column.mRender = renderEmptyCell; + if (opts.width) + column.sWidth = opts.width; + column.bVisible = opts.visible === undefined ? true : opts.visible; + if (opts.sortable !== undefined) + column.bSortable = opts.sortable; + if (opts.searchable !== undefined) + column.bSearchable = opts.searchable; + + if (opts.type && column.bVisible ) { + switch(opts.type) { + case 'date': + column.sType = 'date'; + column.mRender = renderDate(api.tools.djangoFormat(get_format('SHORT_DATE_FORMAT'))); + break; + case 'datetime': + column.sType = 'date'; + column.mRender = renderDate(api.tools.djangoFormat(get_format('SHORT_DATETIME_FORMAT'))); + break; + case 'time': + column.mRender = renderDate(api.tools.djangoFormat(get_format('TIME_FORMAT'))); + break; + case 'iconType': + //columnt.sType = 'html'; // html is default, so this is not needed + column.mRender = renderTypeIcon; + break; + case 'icon': + if( opts.icon !== undefined ) { + column.mRender = renderIcon(opts.icon); + } + break; + case 'dict': + if( opts.dict !== undefined ) { + column.mRender = renderTextTransform(opts.dict); + } + break; + default: + column.sType = opts.type; + } + } + columns.push(column); + } + }); + // Responsive style for tables, using tables.css and this code generates the "titles" for vertical display on small sizes + $('#style-' + tableId).remove(); // Remove existing style for table before adding new one + $(api.templates.evaluate('tmpl_responsive_table', { + tableId: tableId, + columns: columns, + })).appendTo('head'); + + $this.rest.overview(function(data) { + var table = gui.table(title, tableId); + if (options.container === undefined) { + gui.appendToWorkspace('
' + table.text + '
'); + } else { + $('#' + options.container).empty(); + $('#' + options.container).append(table.text); + } + + // What execute on refresh button push + var onRefresh = options.onRefresh || function(){}; + + var refreshFnc = function() { + // Refreshes table content + var tbl = $('#' + tableId).dataTable(); + // Clears selection first + TableTools.fnGetInstance(tableId).fnSelectNone(); + if( data.length > 1000 ) + api.tools.blockUI(); + + $this.rest.overview(function(data) { + /*$(btn).removeClass('disabled').width('').html(saved);*/ + setTimeout( function() { + tbl.fnClearTable(); + tbl.fnAddData(data); + onRefresh($this); + api.tools.unblockUI(); + }, 0); + }); + return false; // This may be used on button or href, better disable execution of it + }; + + var btns = []; + + if (options.buttons) { + var clickHandlerFor = function(handler, action, newHandler) { + var handleFnc = handler || function(val, action, tbl) {gui.doLog('Default handler called for ', action);}; + return function(btn) { + var tbl = $('#' + tableId).dataTable(); + var val = this.fnGetSelectedData()[0]; + setTimeout(function() { + if( newHandler ) { + handleFnc(action, tbl, refreshFnc); + } else { + handleFnc(val, action, tbl, refreshFnc); + } + }, 0); + }; + }; + + // methods for buttons on row select + var editSelected = function(btn, obj, node) { + var sel = this.fnGetSelectedData(); + if (sel.length == 1) { + $(btn).removeClass('disabled').addClass('btn3d-success'); + } else { + $(btn).removeClass('btn3d-success').addClass('disabled'); + } + }; + var deleteSelected = function(btn, obj, node) { + var sel = this.fnGetSelectedData(); + if (sel.length > 0) { + $(btn).removeClass('disabled').addClass('btn3d-warning'); + } else { + $(btn).removeClass('btn3d-warning').addClass('disabled'); + } + }; + + $.each(options.buttons, function(index, value) { + var btn; + switch (value) { + case 'new': + if(Object.keys($this.types).length === 0) { + btn = { + "sExtends" : "text", + "sButtonText" : gui.config.dataTableButtons['new'].text, + "fnClick" : clickHandlerFor(options.onNew, 'new'), + "sButtonClass" : gui.config.dataTableButtons['new'].css, + }; + } else { + // This table has "types, so we create a dropdown with Types + var newButtons = []; + // Order buttons by name, much more easy for users... :-) + var order = []; + $.each($this.types, function(k, v){ + order.push({ + type: k, + css: v.css, + name: v.name, + description: v.description, + }); + }); + $.each(order.sort(function(a,b){return a.name.localeCompare(b.name);}), function(i, val){ + newButtons.push({ + "sExtends" : "text", + "sButtonText" : ' ' + val.name + '', + "fnClick" : clickHandlerFor(options.onNew, val.type, true), + }); + }); + btn = { + "sExtends" : "collection", + "aButtons": newButtons, + "sButtonText" : gui.config.dataTableButtons['new'].text, + "sButtonClass" : gui.config.dataTableButtons['new'].css, + }; + } + break; + case 'edit': + btn = { + "sExtends" : "text", + "sButtonText" : gui.config.dataTableButtons.edit.text, + "fnSelect" : editSelected, + "fnClick" : clickHandlerFor(options.onEdit, 'edit'), + "sButtonClass" : gui.config.dataTableButtons.edit.css, + }; + break; + case 'delete': + btn = { + "sExtends" : "text", + "sButtonText" : gui.config.dataTableButtons['delete'].text, + "fnSelect" : deleteSelected, + "fnClick" : clickHandlerFor(options.onDelete, 'delete'), + "sButtonClass" : gui.config.dataTableButtons['delete'].css, + }; + break; + case 'refresh': + btn = { + "sExtends" : "text", + "sButtonText" : gui.config.dataTableButtons.refresh.text, + "fnClick" : refreshFnc, + "sButtonClass" : gui.config.dataTableButtons.refresh.css, + }; + break; + case 'xls': + btn = { + "sExtends" : "text", + "sButtonText" : gui.config.dataTableButtons.xls.text, + "fnClick" : function(){ + api.templates.get('spreadsheet', function(tmpl) { + var styles = { 'bold': 's21', }; + var uri = 'data:application/vnd.ms-excel;base64,', + base64 = function(s) { return window.btoa(unescape(encodeURIComponent(s))); }; + + var headings = [], rows = []; + $.each(columns, function(index, heading){ + if( heading.bVisible === false ) { + return; + } + headings.push(api.spreadsheet.cell(heading.sTitle, 'String', styles.bold)); + }); + rows.push(api.spreadsheet.row(headings)); + $.each(data, function(index, row) { + var cells = []; + $.each(columns, function(index, col){ + if( col.bVisible === false ) { + return; + } + var type = col.sType == 'numeric' ? 'Number':'String'; + cells.push(api.spreadsheet.cell(row[col.mData], type)); + }); + rows.push(api.spreadsheet.row(cells)); + }); + + var ctx = { + creation_date: (new Date()).toISOString(), + worksheet: title, + columns_count: headings.length, + rows_count: rows.length, + rows: rows.join('\n') + }; + setTimeout( function() { + saveAs(new Blob([api.templates.evaluate(tmpl, ctx)], + {type: 'application/vnd.ms-excel'} ), title + '.xls'); + }, 20); + }); + }, + "sButtonClass" : gui.config.dataTableButtons.xls.css, + }; + } + + if(btn) { + btns.push(btn); + } + }); + } + + // Initializes oTableTools + var oTableTools = { + "aButtons" : btns + }; + + // Type of row selection + if (options.rowSelect) { + oTableTools.sRowSelect = options.rowSelect; + } + + if (options.onRowSelect) { + var rowSelectedFnc = options.onRowSelect; + oTableTools.fnRowSelected = function() { + rowSelectedFnc(this.fnGetSelectedData(), $('#' + tableId).dataTable(), this); + }; + } + if (options.onRowDeselect) { + var rowDeselectedFnc = options.onRowDeselect; + oTableTools.fnRowDeselected = function() { + rowDeselectedFnc(this.fnGetSelectedData(), $('#' + tableId).dataTable(), this); + }; + } + + $('#' + tableId).dataTable({ + "aaData" : data, + "aoColumns" : columns, + "oLanguage" : gui.config.dataTablesLanguage, + "oTableTools" : oTableTools, + // First is upper row, + // second row is lower + // (pagination) row + "sDom" : "<'row'<'col-xs-8'T><'col-xs-4'f>r>t<'row'<'col-xs-5'i><'col-xs-7'p>>", + + }); + // Fix 3dbuttons + api.tools.fix3dButtons('#' + tableId + '_wrapper .btn-group-3d'); + // Fix form + $('#' + tableId + '_filter input').addClass('form-control'); + // Add refresh action to panel + $(table.refreshSelector).click(refreshFnc); + // Add tooltips to "new" buttons + $('.DTTT_dropdown [data-toggle="tooltip"]').tooltip({ + container:'body', + delay: { show: 1000, hide: 100}, + placement: 'auto right', + }); + + if (options.scrollToTable === true ) { + var tableTop = $('#' + tableId).offset().top; + $('html, body').scrollTop(tableTop); + } + // if table rendered event + if( options.onLoad ) { + options.onLoad($this); + } + }); + }); + return '#' + tableId; + } + +}; diff --git a/server/src/uds/static/adm/js/gui.js b/server/src/uds/static/adm/js/gui.js index ebb155c3e..0c362e0e5 100644 --- a/server/src/uds/static/adm/js/gui.js +++ b/server/src/uds/static/adm/js/gui.js @@ -97,11 +97,24 @@ }); }; - gui.modal = function(id, title, content) { + gui.modal = function(id, title, content, actionButton, closeButton) { return api.templates.evaluate('tmpl_modal', { id: id, title: title, - content: content + content: content, + button1: closeButton, + button2: actionButton + }); + }; + + gui.launchModal = function(title, content, actionButton, closeButton) { + var id = Math.random().toString().split('.')[1]; // Get a random ID for this modal + gui.appendToWorkspace(gui.modal(id, title, content, actionButton, closeButton)); + id = '#' + id; // for jQuery + + $(id).modal() + .on('hidden.bs.modal', function () { + $(id).remove(); }); }; @@ -143,10 +156,12 @@ if( !$form.valid() ) return; if( onSuccess ) { - if( onSuccess(id + ' form') === false ) // Some error may have ocurred, do not close dialog + onSuccess(id + ' form', function(){$(id).modal('hide');}); // Delegate close to to onSuccess return; + } else { + $(id).modal('hide'); } - $(id).modal('hide'); + }); // Launch modal @@ -156,6 +171,16 @@ }); }; + gui.failRequestMessageFnc = function(jqXHR, textStatus, errorThrown) { + api.templates.get('request_failed', function(tmpl) { + gui.clearWorkspace(); + gui.appendToWorkspace(api.templates.evaluate(tmpl, { + error: jqXHR.responseText, + })); + }); + gui.setLinksEvents(); + }; + gui.clearWorkspace = function() { $('#content').empty(); $('#minimized').empty(); @@ -233,432 +258,3 @@ gui.debug = true; }(window.gui = window.gui || {}, jQuery)); -function BasicGuiElement(name) { - "use strict"; - this.name = name; -} - -function GuiElement(restItem, name) { - "use strict"; - this.rest = restItem; - this.name = name; - this.types = {}; - this.init(); -} - -// all gui elements has, at least, name && type -// Types must include, at least: type, icon -GuiElement.prototype = { - init : function() { - "use strict"; - gui.doLog('Initializing ' + this.name); - var $this = this; - this.rest.types(function(data) { - var styles = ''; - $.each(data, function(index, value) { - var className = $this.name + '-' + value.type; - $this.types[value.type] = { - css : className, - name : value.name || '', - description : value.description || '' - }; - gui.doLog('Creating style for ' + className); - var style = '.' + className + ' { display:inline-block; background: url(data:image/png;base64,' + - value.icon + '); ' + 'width: 16px; height: 16px; vertical-align: middle; } '; - styles += style; - }); - if (styles !== '') { - styles = ''; - $(styles).appendTo('head'); - } - }); - }, - // Options: dictionary - // container: container ID of parent for the table. If undefined, table will be appended to workspace - // buttons: array of visible buttons (strings), valid are [ 'new', 'edit', 'refresh', 'delete', 'xls' ], - // rowSelect: type of allowed row selection, valid values are 'single' and 'multi' - // scrollToTable: if True, will scroll page to show table - // - // onLoad: Event (function). If defined, will be invoked when table is fully loaded. - // Receives 1 parameter, that is the gui element (GuiElement) used to render table - // onRowSelect: Event (function). If defined, will be invoked when a row of table is selected - // Receives 3 parameters: - // 1.- the array of selected items data (objects, as got from api...get) - // 2.- the DataTable that raised the event - // 3.- the DataTableTools that raised the event - // onRowDeselect: Event (function). If defined, will be invoked when a row of table is deselected - // Receives 3 parameters: - // 1.- the array of selected items data (objects, as got from api...get) - // 2.- the DataTable that raised the event - // onNew: Event (function). If defined, will be invoked when "new" button is pressed - // Receives 4 parameters: - // 1.- the selected item data (single object, as got from api...get) - // 2.- the event that fired this (new, delete, edit, ..) - // 3.- the DataTable that raised the event - // onEdit: Event (function). If defined, will be invoked when "edit" button is pressed - // Receives 4 parameters: - // 1.- the selected item data (single object, as got from api...get) - // 2.- the event that fired this (new, delete, edit, ..) - // 3.- the DataTable that raised the event - // onDelete: Event (function). If defined, will be invoked when "delete" button is pressed - // Receives 4 parameters: - // 1.- the selected item data (single object, as got from api...get) - // 2.- the event that fired this (new, delete, edit, ..) - // 4.- the DataTable that raised the event - table : function(options) { - "use strict"; - gui.doLog('Types: ', this.types); - options = options || {}; - gui.doLog('Composing table for ' + this.name); - var tableId = this.name + '-table'; - var $this = this; // Store this for child functions - - // Empty cells transform - var renderEmptyCell = function(data) { - if( data === '' ) - return '-'; - return data; - }; - - // Datetime renderer (with specified format) - var renderDate = function(format) { - return function(data, type, full) { - return api.tools.strftime(format, new Date(data*1000)); - }; - }; - - // Icon renderer, based on type (created on init methods in styles) - var renderTypeIcon = function(data, type, value){ - if( type == 'display' ) { - var css = $this.types[value.type].css; - return ' ' + renderEmptyCell(data); - } else { - return renderEmptyCell(data); - } - }; - - // Custom icon renderer, in fact span with defined class - var renderIcon = function(icon) { - return function(data, type, full) { - if( type == 'display' ) { - return ' ' + renderEmptyCell(data); - } else { - return renderEmptyCell(data); - } - }; - }; - // Text transformation, dictionary based - var renderTextTransform = function(dict) { - return function(data, type, full) { - return dict[data] || renderEmptyCell(''); - }; - }; - this.rest.tableInfo(function(data) { - var title = data.title; - var columns = []; - $.each(data.fields, function(index, value) { - for ( var v in value) { - var opts = value[v]; - var column = { - mData : v, - }; - column.sTitle = opts.title; - column.mRender = renderEmptyCell; - if (opts.width) - column.sWidth = opts.width; - column.bVisible = opts.visible === undefined ? true : opts.visible; - if (opts.sortable !== undefined) - column.bSortable = opts.sortable; - if (opts.searchable !== undefined) - column.bSearchable = opts.searchable; - - if (opts.type && column.bVisible ) { - switch(opts.type) { - case 'date': - column.sType = 'date'; - column.mRender = renderDate(api.tools.djangoFormat(get_format('SHORT_DATE_FORMAT'))); - break; - case 'datetime': - column.sType = 'date'; - column.mRender = renderDate(api.tools.djangoFormat(get_format('SHORT_DATETIME_FORMAT'))); - break; - case 'time': - column.mRender = renderDate(api.tools.djangoFormat(get_format('TIME_FORMAT'))); - break; - case 'iconType': - //columnt.sType = 'html'; // html is default, so this is not needed - column.mRender = renderTypeIcon; - break; - case 'icon': - if( opts.icon !== undefined ) { - column.mRender = renderIcon(opts.icon); - } - break; - case 'dict': - if( opts.dict !== undefined ) { - column.mRender = renderTextTransform(opts.dict); - } - break; - default: - column.sType = opts.type; - } - } - columns.push(column); - } - }); - // Responsive style for tables, using tables.css and this code generates the "titles" for vertical display on small sizes - $('#style-' + tableId).remove(); // Remove existing style for table before adding new one - $(api.templates.evaluate('tmpl_responsive_table', { - tableId: tableId, - columns: columns, - })).appendTo('head'); - - $this.rest.overview(function(data) { - var table = gui.table(title, tableId); - if (options.container === undefined) { - gui.appendToWorkspace('
' + table.text + '
'); - } else { - $('#' + options.container).empty(); - $('#' + options.container).append(table.text); - } - - // What execute on refresh button push - var onRefresh = options.onRefresh || function(){}; - - var refreshFnc = function() { - // Refreshes table content - var tbl = $('#' + tableId).dataTable(); - // Clears selection first - TableTools.fnGetInstance(tableId).fnSelectNone(); - if( data.length > 1000 ) - api.tools.blockUI(); - - $this.rest.overview(function(data) { - /*$(btn).removeClass('disabled').width('').html(saved);*/ - setTimeout( function() { - tbl.fnClearTable(); - tbl.fnAddData(data); - onRefresh($this); - api.tools.unblockUI(); - }, 0); - }); - return false; // This may be used on button or href, better disable execution of it - }; - - var btns = []; - - if (options.buttons) { - var clickHandlerFor = function(handler, action, newHandler) { - var handleFnc = handler || function(val, action, tbl) {gui.doLog('Default handler called for ', action);}; - return function(btn) { - var tbl = $('#' + tableId).dataTable(); - var val = this.fnGetSelectedData()[0]; - setTimeout(function() { - if( newHandler ) { - if( handleFnc(action, tbl) === true ) // Reload table? - refreshFnc(); - } else { - if( handleFnc(val, action, tbl) === true ) // Reload table? - refreshFnc(); - } - }, 0); - }; - }; - - // methods for buttons on row select - var editSelected = function(btn, obj, node) { - var sel = this.fnGetSelectedData(); - if (sel.length == 1) { - $(btn).removeClass('disabled').addClass('btn3d-success'); - } else { - $(btn).removeClass('btn3d-success').addClass('disabled'); - } - }; - var deleteSelected = function(btn, obj, node) { - var sel = this.fnGetSelectedData(); - if (sel.length > 0) { - $(btn).removeClass('disabled').addClass('btn3d-warning'); - } else { - $(btn).removeClass('btn3d-warning').addClass('disabled'); - } - }; - - $.each(options.buttons, function(index, value) { - var btn; - switch (value) { - case 'new': - if(Object.keys($this.types).length === 0) { - btn = { - "sExtends" : "text", - "sButtonText" : gui.config.dataTableButtons['new'].text, - "fnClick" : clickHandlerFor(options.onNew, 'new'), - "sButtonClass" : gui.config.dataTableButtons['new'].css, - }; - } else { - // This table has "types, so we create a dropdown with Types - var newButtons = []; - // Order buttons by name, much more easy for users... :-) - var order = []; - $.each($this.types, function(k, v){ - order.push({ - type: k, - css: v.css, - name: v.name, - description: v.description, - }); - }); - $.each(order.sort(function(a,b){return a.name.localeCompare(b.name);}), function(i, val){ - newButtons.push({ - "sExtends" : "text", - "sButtonText" : ' ' + val.name + '', - "fnClick" : clickHandlerFor(options.onNew, val.type, true), - }); - }); - btn = { - "sExtends" : "collection", - "aButtons": newButtons, - "sButtonText" : gui.config.dataTableButtons['new'].text, - "sButtonClass" : gui.config.dataTableButtons['new'].css, - }; - } - break; - case 'edit': - btn = { - "sExtends" : "text", - "sButtonText" : gui.config.dataTableButtons.edit.text, - "fnSelect" : editSelected, - "fnClick" : clickHandlerFor(options.onEdit, 'edit'), - "sButtonClass" : gui.config.dataTableButtons.edit.css, - }; - break; - case 'delete': - btn = { - "sExtends" : "text", - "sButtonText" : gui.config.dataTableButtons['delete'].text, - "fnSelect" : deleteSelected, - "fnClick" : clickHandlerFor(options.onDelete, 'delete'), - "sButtonClass" : gui.config.dataTableButtons['delete'].css, - }; - break; - case 'refresh': - btn = { - "sExtends" : "text", - "sButtonText" : gui.config.dataTableButtons.refresh.text, - "fnClick" : refreshFnc, - "sButtonClass" : gui.config.dataTableButtons.refresh.css, - }; - break; - case 'xls': - btn = { - "sExtends" : "text", - "sButtonText" : gui.config.dataTableButtons.xls.text, - "fnClick" : function(){ - api.templates.get('spreadsheet', function(tmpl) { - var styles = { 'bold': 's21', }; - var uri = 'data:application/vnd.ms-excel;base64,', - base64 = function(s) { return window.btoa(unescape(encodeURIComponent(s))); }; - - var headings = [], rows = []; - $.each(columns, function(index, heading){ - if( heading.bVisible === false ) { - return; - } - headings.push(api.spreadsheet.cell(heading.sTitle, 'String', styles.bold)); - }); - rows.push(api.spreadsheet.row(headings)); - $.each(data, function(index, row) { - var cells = []; - $.each(columns, function(index, col){ - if( col.bVisible === false ) { - return; - } - var type = col.sType == 'numeric' ? 'Number':'String'; - cells.push(api.spreadsheet.cell(row[col.mData], type)); - }); - rows.push(api.spreadsheet.row(cells)); - }); - - var ctx = { - creation_date: (new Date()).toISOString(), - worksheet: title, - columns_count: headings.length, - rows_count: rows.length, - rows: rows.join('\n') - }; - setTimeout( function() { - saveAs(new Blob([api.templates.evaluate(tmpl, ctx)], - {type: 'application/vnd.ms-excel'} ), title + '.xls'); - }, 20); - }); - }, - "sButtonClass" : gui.config.dataTableButtons.xls.css, - }; - } - - if(btn) { - btns.push(btn); - } - }); - } - - // Initializes oTableTools - var oTableTools = { - "aButtons" : btns - }; - - // Type of row selection - if (options.rowSelect) { - oTableTools.sRowSelect = options.rowSelect; - } - - if (options.onRowSelect) { - var rowSelectedFnc = options.onRowSelect; - oTableTools.fnRowSelected = function() { - rowSelectedFnc(this.fnGetSelectedData(), $('#' + tableId).dataTable(), this); - }; - } - if (options.onRowDeselect) { - var rowDeselectedFnc = options.onRowDeselect; - oTableTools.fnRowDeselected = function() { - rowDeselectedFnc(this.fnGetSelectedData(), $('#' + tableId).dataTable(), this); - }; - } - - $('#' + tableId).dataTable({ - "aaData" : data, - "aoColumns" : columns, - "oLanguage" : gui.config.dataTablesLanguage, - "oTableTools" : oTableTools, - // First is upper row, - // second row is lower - // (pagination) row - "sDom" : "<'row'<'col-xs-8'T><'col-xs-4'f>r>t<'row'<'col-xs-5'i><'col-xs-7'p>>", - - }); - // Fix 3dbuttons - api.tools.fix3dButtons('#' + tableId + '_wrapper .btn-group-3d'); - // Fix form - $('#' + tableId + '_filter input').addClass('form-control'); - // Add refresh action to panel - $(table.refreshSelector).click(refreshFnc); - // Add tooltips to "new" buttons - $('.DTTT_dropdown [data-toggle="tooltip"]').tooltip({ - container:'body', - delay: { show: 1000, hide: 100}, - placement: 'auto right', - }); - - if (options.scrollToTable === true ) { - var tableTop = $('#' + tableId).offset().top; - $('html, body').scrollTop(tableTop); - } - // if table rendered event - if( options.onLoad ) { - options.onLoad($this); - } - }); - }); - return '#' + tableId; - } - -}; diff --git a/server/src/uds/templates/uds/admin/index.html b/server/src/uds/templates/uds/admin/index.html index 58b81d6e3..183b726dc 100644 --- a/server/src/uds/templates/uds/admin/index.html +++ b/server/src/uds/templates/uds/admin/index.html @@ -51,7 +51,7 @@ - + @@ -99,15 +99,17 @@ + - + {% block js %}{% endblock %} @@ -116,6 +118,8 @@ {% js_template 'dashboard' %} {% js_template 'authenticators' %} + + {% js_template 'request_failed' %} {% js_template 'table' %} {% js_template 'modal' %} diff --git a/server/src/uds/templates/uds/admin/tmpl/modal.html b/server/src/uds/templates/uds/admin/tmpl/modal.html index 36c99c5a6..ffae71abf 100644 --- a/server/src/uds/templates/uds/admin/tmpl/modal.html +++ b/server/src/uds/templates/uds/admin/tmpl/modal.html @@ -19,7 +19,7 @@ {% verbatim %} {{/ if }} {{# if button2 }} - {{{ button1 }}} + {{{ button2 }}} {{ else }} {% endverbatim %} diff --git a/server/src/uds/templates/uds/admin/tmpl/request_failed.html b/server/src/uds/templates/uds/admin/tmpl/request_failed.html new file mode 100644 index 000000000..7f1b70943 --- /dev/null +++ b/server/src/uds/templates/uds/admin/tmpl/request_failed.html @@ -0,0 +1,10 @@ +{% load i18n %} + +
+

{% trans 'Error on request' %}

+
+ {% verbatim %}

{{ error }}

{% endverbatim %} +
{% trans 'There was an error requesting data from server, please, try again' %}
+
+ {% trans "Dashboard" %} +