* Implemented services REST part

* Added possibility of creating own types for DetailApi (useful for provicers/services, where services is a detail but also has types)
More refactoring
This commit is contained in:
Adolfo Gómez 2013-11-27 11:14:53 +00:00
parent d46400c1f7
commit 7ea7086ba9
13 changed files with 306 additions and 106 deletions

View File

@ -15,6 +15,7 @@ encoding//src/uds/REST/methods/login_logout.py=utf-8
encoding//src/uds/REST/methods/networks.py=utf-8
encoding//src/uds/REST/methods/osmanagers.py=utf-8
encoding//src/uds/REST/methods/providers.py=utf-8
encoding//src/uds/REST/methods/services.py=utf-8
encoding//src/uds/REST/methods/transports.py=utf-8
encoding//src/uds/REST/methods/users_groups.py=utf-8
encoding//src/uds/REST/mixins.py=utf-8

View File

@ -34,6 +34,7 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext, ugettext_lazy as _
from uds.models import Provider
from services import Services
from uds.core import services
from uds.REST import Handler, NotFound
@ -46,12 +47,23 @@ logger = logging.getLogger(__name__)
class Providers(ModelHandlerMixin, Handler):
model = Provider
detail = { 'services': Services }
save_fields = ['name', 'comments']
def item_as_dict(self, provider):
type_ = provider.getType()
# Icon can have a lot of data (1-2 Kbytes), but it's not expected to have a lot of services providers, and even so, this will work fine
offers = [{
'name' : ugettext(t.name()),
'type' : t.type(),
'description' : ugettext(t.description()),
'icon' : t.icon().replace('\n', '') } for t in type_.getServicesTypes()]
return { 'id': provider.id,
'name': provider.name,
'services_count': provider.services.count(),
'offers': offers,
'type': type_.type(),
'comments': provider.comments,
}
@ -64,13 +76,15 @@ class Types(ModelTypeHandlerMixin, Handler):
def getGui(self, type_):
try:
return services.factory().lookup(type_).guiDescription()
return self.addDefaultFields(services.factory().lookup(type_).guiDescription(), ['name', 'comments'])
except:
raise NotFound('type not found')
class TableInfo(ModelTableHandlerMixin, Handler):
path = 'providers'
detail = { 'services': Services }
title = _('Current service providers')
fields = [
{ 'name': {'title': _('Name'), 'type': 'iconType' } },
{ 'comments': {'title': _('Comments')}},

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@provideror: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from uds.models import Provider
from uds.REST.mixins import DetailHandler
import logging
logger = logging.getLogger(__name__)
class Services(DetailHandler):
def get(self):
# Extract providerenticator
provider = self._kwargs['parent']
try:
if len(self._args) == 0:
return [{
'id':k.id,
'name': k.name,
'comments': k.comments,
'type': k.data_type,
'typeName' : _(k.getType().name())
} for k in provider.services.all() ]
else:
with provider.get(pk=self._args[0]) as k:
return {
'id':k.id,
'name': k.name,
'comments': k.comments,
'type': k.data_type,
'typeName' : _(k.getType().name())
}
except:
logger.exception('En services')
return { 'error': 'not found' }
def getTitle(self):
try:
return _('Services of {0}').format(Provider.objects.get(pk=self._kwargs['parent_id']).name)
except:
return _('Current services')
def getFields(self):
return [
{ 'name': {'title': _('Service name'), 'visible': True, 'type': 'iconType' } },
{ 'comments': { 'title': _('Comments') } },
{ 'type': {'title': _('Type') } }
]

View File

@ -56,7 +56,7 @@ class Transports(ModelHandlerMixin, Handler):
'comments': item.comments,
'priority': item.priority,
'nets_positive': item.nets_positive,
'networks': [ {'id': k.id} for k in item.networks.all() ],
'networks': [ n.id for n in item.networks.all() ],
'deployed_count': item.deployedServices.count(),
'type': type_.type(),
}
@ -64,7 +64,6 @@ class Transports(ModelHandlerMixin, Handler):
class Types(ModelTypeHandlerMixin, Handler):
path = 'transports'
has_comments = True
def enum_types(self):
return factory().providers().values()

View File

@ -183,7 +183,6 @@ class ModelTypeHandlerMixin(object):
'''
authenticated = True
needs_staff = True
has_comments = False
def enum_types(self):
pass

View File

@ -767,6 +767,10 @@ class UserInterface(object):
val = '\001' + cPickle.dumps(v.value)
else:
val = v.value
if val is True:
val = gui.TRUE
elif val is False:
val = gui.FALSE
arr.append(k + '\003' + val)
return '\002'.join(arr).encode('zip')

View File

@ -59,12 +59,16 @@ table.dataTable tr.even td.sorting_3 { background-color: blue; }*/
margin-bottom: 0.3em;
}
/* modal dialogs & related*/
.modal-dialog {
/* new custom width */
width: 60%;
}
.tab-pane {
margin-top: 24px;
}
.tooltip {
z-index: 2014;
}

View File

@ -331,16 +331,18 @@ BasicModelRest.prototype = {
});
},
detail: function(id, child) {
detail: function(id, child, options) {
"use strict";
return new DetailModelRestApi(this, id, child);
options = options || {};
return new DetailModelRestApi(this, id, child, options);
}
};
// For REST of type /auth/[id]/users, /services/[id]/users, ...
function DetailModelRestApi(parentApi, parentId, model) {
function DetailModelRestApi(parentApi, parentId, model, options) {
"use strict";
this.options = options;
this.base = new BasicModelRest(undefined, {
getPath: [parentApi.path, parentId, model].join('/'),
typesPath: '.', // We do not has this on details
@ -350,29 +352,33 @@ function DetailModelRestApi(parentApi, parentId, model) {
DetailModelRestApi.prototype = {
// Generates a basic model with fixed methods for "detail" models
get: function(options) {
get: function(success_fnc, fail_fnc) {
"use strict";
return this.base.get(options);
return this.base.get(success_fnc, fail_fnc);
},
tableInfo: function(options) {
tableInfo: function(success_fnc, fail_fnc) {
"use strict";
return this.base.tableInfo(options);
return this.base.tableInfo(success_fnc, fail_fnc);
},
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";
return this.base.list(success_fnc, options);
return this.base.list(success_fnc, fail_fnc);
},
overview: function(success_fnc, options) {
overview: function(success_fnc, fail_fnc) {
"use strict";
return this.base.overview(success_fnc, options);
return this.base.overview(success_fnc, fail_fnc);
},
item: function(itemId, success_fnc, options) {
item: function(itemId, success_fnc, fail_fnc) {
"use strict";
return this.base.item(success_fnc, options);
return this.base.item(success_fnc, fail_fnc);
},
types: function(options) {
types: function(success_fnc, fail_fnc) {
"use strict";
return this.base.types(options);
if( this.options.types ) {
this.options.types(success_fnc, fail_fnc);
} else {
return this.base.types(success_fnc, fail_fnc);
}
},
};

View File

@ -29,22 +29,60 @@ gui.dashboard.link = function(event) {
gui.providers = new GuiElement(api.providers, 'provi');
gui.providers.link = function(event) {
"use strict";
gui.clearWorkspace();
gui.appendToWorkspace(gui.breadcrumbs(gettext('Service Providers')));
api.templates.get('providers', function(tmpl) {
gui.clearWorkspace();
gui.appendToWorkspace(api.templates.evaluate(tmpl, {
providers : 'providers-placeholder',
services : 'services-placeholder',
}));
gui.setLinksEvents();
var tableId = gui.providers.table({
rowSelect : 'single',
onEdit: function(value, event, table) {
gui.providers.rest.gui(value.type, function(data) {
var form = gui.fields(data);
gui.appendToWorkspace(gui.modal('edit_modal', gettext('Edit service provider'), form));
$('#edit_modal').modal()
.on('hidden.bs.modal', function () {
$('#edit_modal').remove();
var tableId = gui.providers.table({
container : 'providers-placeholder',
rowSelect : 'single',
onRowSelect : function(selected) {
api.tools.blockUI();
gui.doLog(selected[0]);
var id = selected[0].id;
// Options for detail, to initialize types correctly
var detail_options = {
types: function(success_fnc, fail_fnc) {
success_fnc(selected[0].offers);
}
};
// Giving the name compossed with type, will ensure that only styles will be reattached once
var services = new GuiElement(api.providers.detail(id, 'services', detail_options), 'services-'+selected[0].type);
services.table({
container : 'services-placeholder',
rowSelect : 'single',
buttons : [ 'new', 'edit', 'delete', 'xls' ],
scrollToTable : false,
onLoad: function(k) {
api.tools.unblockUI();
},
});
return false;
},
buttons : [ 'new', 'edit', 'delete', 'xls' ],
onEdit: function(value, event, table, refreshFnc) {
gui.providers.rest.gui(value.type, function(itemGui){
gui.providers.rest.item(value.id, function(item) {
gui.forms.launchModal(gettext('Edit Service Provider')+' '+value.name, itemGui, item, function(form_selector, closeFnc) {
var fields = gui.forms.read(form_selector);
fields.data_type = value.type;
fields.nets_positive = false;
gui.providers.rest.save(fields, function(data) { // Success on put
closeFnc();
refreshFnc();
}, gui.failRequestModalFnc(gettext('Error creating Service Provider')) // Fail on put, show modal message
);
return false;
});
});
});
},
buttons : [ 'edit', 'delete', 'xls' ],
});
},
});
});
return false;
@ -157,9 +195,8 @@ gui.connectivity.link = function(event) {
},
]
};
var form = gui.form.fromFields(tabs, item);
gui.launchModalForm(gettext('Edit transport')+' '+value.name,form, function(form_selector, closeFnc) {
var fields = gui.form.read(form_selector);
gui.forms.launchModal(gettext('Edit transport')+' '+value.name, tabs, item, function(form_selector, closeFnc) {
var fields = gui.forms.read(form_selector);
fields.data_type = value.type;
fields.nets_positive = false;
gui.connectivity.transports.rest.save(fields, function(data) { // Success on put
@ -182,12 +219,17 @@ gui.connectivity.link = function(event) {
},
{
title: 'Networks',
fields: [],
fields: [
gui.forms.guiField('networks', 'multichoice', gettext('Available for networks'),
gettext('Select networks that will see this transport'), [], []),
gui.forms.guiField('nets_positive', 'checkbox', gettext('Transport active for selected networks'),
gettext('If active, transport will only be available on selected networks. If inactive, transport will be available form any net EXCEPT selected networks'),
true)
],
},
]
};
var form = gui.form.fromFields(tabs);
gui.launchModalForm(gettext('New transport'), form, function(form_selector, closeFnc) {
gui.forms.launchModal(gettext('New transport'), tabs, undefined, function(form_selector, closeFnc) {
var fields = gui.form.read(form_selector);
// Append "own" fields, in this case data_type
fields.data_type = type;

View File

@ -5,7 +5,7 @@ function BasicGuiElement(name) {
this.name = name;
}
function GuiElement(restItem, name) {
function GuiElement(restItem, name, typesFunction) {
"use strict";
this.rest = restItem;
this.name = name;
@ -22,6 +22,7 @@ GuiElement.prototype = {
var self = this;
this.rest.types(function(data) {
var styles = '';
var alreadyAttached = $('#gui-style-'+self.name).length !== 0;
$.each(data, function(index, value) {
var className = self.name + '-' + value.type;
self.types[value.type] = {
@ -30,12 +31,15 @@ GuiElement.prototype = {
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( !alreadyAttached ) {
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 = '<style media="screen">' + styles + '</style>';
// If style already attached, do not re-attach it
styles = '<style id="gui-style-' + self.name + '" media="screen">' + styles + '</style>';
$(styles).appendTo('head');
}
});
@ -101,7 +105,8 @@ GuiElement.prototype = {
// Icon renderer, based on type (created on init methods in styles)
var renderTypeIcon = function(data, type, value){
if( type == 'display' ) {
var css = self.types[value.type].css;
self.types[value.type] = self.types[value.type] || {}
var css = self.types[value.type].css || 'fa fa-asterisk';
return '<span class="' + css + '"></span> ' + renderEmptyCell(data);
} else {
return renderEmptyCell(data);

View File

@ -2,8 +2,10 @@
(function(gui, $, undefined) {
"use strict";
// Returns a form that will manage a gui description (new or edit)
gui.fieldsToHtml = function(itemGui, item, editing) {
gui.forms = {};
// Returns form fields that will manage a gui description (new or edit)
gui.forms.fieldsToHtml = function(itemGui, item, editing) {
var html = '';
// itemGui is expected to have fields sorted by .gui.order (REST api returns them sorted)
$.each(itemGui, function(index, f){
@ -30,9 +32,7 @@
return html;
};
gui.form = {};
gui.form.fromFields = function(fields, item) {
gui.forms.fromFields = function(fields, item) {
var editing = item !== undefined; // Locate real Editing
item = item || {id:''};
var form = '<form class="form-horizontal" role="form">' +
@ -43,20 +43,20 @@
var tabsContent = [];
var active = ' active in' ;
$.each(fields.tabs, function(index, tab){
tabsContent.push('<div class="tab-pane fade' + active + '" id="' + id + index + '">' + gui.fieldsToHtml(tab.fields, item) + '</div>' );
tabsContent.push('<div class="tab-pane fade' + active + '" id="' + id + index + '">' + gui.forms.fieldsToHtml(tab.fields, item) + '</div>' );
tabs.push('<li><a href="#' + id + index + '" data-toggle="tab">' + tab.title + '</a></li>' );
active = '';
});
form += '<ul class="nav nav-tabs">' + tabs.join('\n') + '</ul><div class="tab-content">' + tabsContent.join('\n') + '</div>';
} else {
form += gui.fieldsToHtml(fields, item, editing);
form += gui.forms.fieldsToHtml(fields, item, editing);
}
form += '</form>';
return form;
};
// Reads fields from a form
gui.form.read = function(formSelector) {
gui.forms.read = function(formSelector) {
var res = {};
$(formSelector + ' .modal_field_data').each(function(i, field) {
var $field = $(field);
@ -72,6 +72,82 @@
return res;
};
gui.forms.launchModal = function(title, fields, item, onSuccess) {
var id = 'modal-' + Math.random().toString().split('.')[1]; // Get a random ID for this modal
gui.appendToWorkspace(gui.modal(id, title, gui.forms.fromFields(fields, item)));
id = '#' + id; // for jQuery
// Get form
var $form = $(id + ' form');
// For "beauty" switches, initialize them now
$(id + ' .make-switch').bootstrapSwitch();
// Activate "cool" selects
$(id + ' .selectpicker').selectpicker();
// TEST: cooller on mobile devices
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) ) {
$(id + ' .selectpicker').selectpicker('mobile');
}
// Activate tooltips
$(id + ' [data-toggle="tooltip"]').tooltip({delay: {show: 1000, hide: 100}, placement: 'auto right'});
// Validation
$form.validate({
debug: true,
errorClass: 'text-danger',
validClass: 'has-success',
highlight: function(element) {
$(element).closest('.form-group').addClass('has-error');
},
success: function(element) {
$(element).closest('.form-group').removeClass('has-error');
$(element).remove();
},
});
// And catch "accept" (default is "Save" in fact) button click
$(id + ' .button-accept').click(function(){
if( !$form.valid() )
return;
if( onSuccess ) {
onSuccess(id + ' form', function(){$(id).modal('hide');}); // Delegate close to to onSuccess
return;
} else {
$(id).modal('hide');
}
});
// Launch modal
$(id).modal({keyboard: false})
.on('hidden.bs.modal', function () {
$(id).remove();
});
};
// simple gui generators
gui.forms.guiField = function(name, type, label, tooltip, value, values, length, multiline, readonly, required) {
length = length || 128;
multiline = multiline !== undefined ? multiline : 0;
readonly = readonly || false;
required = required || false;
return {
name: name,
gui: {
defvalue: value,
value: value,
values: values,
label: label,
length: length,
multiline: multiline,
rdonly: readonly, // rdonly applies just to editing
required: required,
tooltip: tooltip,
type: type,
}
};
};
}(window.gui = window.gui || {}, jQuery));

View File

@ -118,58 +118,6 @@
});
};
gui.launchModalForm = function(title, form, onSuccess) {
var id = 'modal-' + Math.random().toString().split('.')[1]; // Get a random ID for this modal
gui.appendToWorkspace(gui.modal(id, title, form));
id = '#' + id; // for jQuery
// Get form
var $form = $(id + ' form');
// For "beauty" switches, initialize them now
$(id + ' .make-switch').bootstrapSwitch();
// Activate "cool" selects
$(id + ' .selectpicker').selectpicker();
// TEST: cooller on mobile devices
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) ) {
$(id + ' .selectpicker').selectpicker('mobile');
}
// Activate tooltips
$(id + ' [data-toggle="tooltip"]').tooltip({delay: {show: 1000, hide: 100}, placement: 'auto right'});
// Validation
$form.validate({
debug: true,
errorClass: 'text-danger',
validClass: 'has-success',
highlight: function(element) {
$(element).closest('.form-group').addClass('has-error');
},
success: function(element) {
$(element).closest('.form-group').removeClass('has-error');
$(element).remove();
},
});
// And catch "accept" (default is "Save" in fact) button click
$(id + ' .button-accept').click(function(){
if( !$form.valid() )
return;
if( onSuccess ) {
onSuccess(id + ' form', function(){$(id).modal('hide');}); // Delegate close to to onSuccess
return;
} else {
$(id).modal('hide');
}
});
// Launch modal
$(id).modal({keyboard: false})
.on('hidden.bs.modal', function () {
$(id).remove();
});
};
gui.failRequestMessageFnc = function(jqXHR, textStatus, errorThrown) {
api.templates.get('request_failed', function(tmpl) {

View File

@ -0,0 +1,18 @@
{% load i18n %}
<div class="row">
<div class="col-xs-12">
<h1>{% trans 'Providers' %}</h1>
<ol class="breadcrumb">
<li><a class="lnk-dashboard" href="#"><i class="fa fa-dashboard"></i> Dashboard</a></li>
<li>{% trans 'Providers' %}</li>
</ol>
</div>
</div><!-- /.row -->
{% verbatim %}
<div class="row">
<div id="{{ providers }}" class="col-xs-12"></div>
</div>
<div class="row">
<div id="{{ services }}" class="col-xs-12"></div>
</div>
{% endverbatim %}