1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Added gui basic elements

refactoring of existint javascript
translations of buttons moved to better place
added css class that will made button text dissapear for smaller displays
This commit is contained in:
Adolfo Gómez 2013-11-20 11:09:30 +00:00
parent 6144eb2f6a
commit 4b2eae58b1
9 changed files with 171 additions and 459 deletions

View File

@ -36,7 +36,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _
from uds.models import Provider
from uds.core import services
from uds.REST import Handler, HandlerError
from uds.REST import Handler, NotFound
from uds.REST.mixins import ModelHandlerMixin, ModelTypeHandlerMixin, ModelTableHandlerMixin
import logging
@ -62,12 +62,18 @@ class Types(ModelTypeHandlerMixin, Handler):
def enum_types(self):
return services.factory().providers().values()
def getGui(self, type_):
try:
return services.factory().lookup(type_).guiDescription()
except:
raise NotFound('type not found')
class TableInfo(ModelTableHandlerMixin, Handler):
path = 'providers'
title = _('Current service providers')
fields = [
{ 'name': {'title': _('Name'), 'type': 'iconType' } },
{ 'comments': {'title': _('Comments')}},
{ 'services_count': {'title': _('Services'), 'type': 'numeric', 'width': '5em'}}
{ 'services_count': {'title': _('Services'), 'type': 'numeric', 'width': '5em'}},
]

View File

@ -145,6 +145,41 @@ class ModelTypeHandlerMixin(object):
if self._args[1] == 'gui':
gui = self.getGui(self._args[0])
# Add name default description, at top of form
gui.insert(0, {
'name': 'name',
'value':'',
'gui': {
'required':True,
'defvalue':'',
'value':'',
'label': _('Name'),
'length': 128,
'multiline': 0,
'tooltip': _('Name of this element'),
'rdonly': False,
'type': 'text',
'order': 1
}
})
# And comments
gui.insert(1, {
'name': 'comments',
'value':'',
'gui': {
'required':True,
'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 gui

View File

@ -92,7 +92,9 @@ table.tablesorter thead tr th:hover {
margin-bottom:12px;
}
.label-tbl-button {
display: none;
}
/* Edit Below to Customize Widths > 768px */
@media (min-width:768px) {
@ -166,4 +168,9 @@ table.tablesorter thead tr th:hover {
white-space: normal;
}
.label-tbl-button {
display: inline-block;
}
}
}

View File

@ -34,8 +34,8 @@ gui.providers.link = function(event) {
var tableId = gui.providers.table({
rowSelect : 'multi',
rowSelectFnc : function(nodes) {
gui.doLog(nodes);
rowSelectFnc : function(data) {
gui.doLog(data);
gui.doLog(this);
gui.doLog(this.fnGetSelectedData());
},
@ -65,15 +65,15 @@ gui.authenticators.link = function(event) {
container : 'auths-placeholder',
rowSelect : 'single',
buttons : [ 'edit', 'refresh', 'delete', 'xls' ],
onRowSelect : function(nodes) {
onRowSelect : function(selected) {
api.tools.blockUI();
var id = this.fnGetSelectedData()[0].id;
var id = selected[0].id;
var user = new GuiElement(api.authenticators.detail(id, 'users'), 'users');
user.table({
container : 'users-placeholder',
rowSelect : 'multi',
buttons : [ 'edit', 'refresh', 'delete', 'xls' ],
scroll : true,
scrollToTable : true,
onLoad: function(k) {
api.tools.unblockUI();
},

View File

@ -12,9 +12,11 @@
}
};
// Several convenience "constants"
gui.dataTablesLanguage = {
gui.config = gui.config || {};
// Several convenience "constants" for tables
gui.config.dataTablesLanguage = {
"sLengthMenu" : gettext("_MENU_ records per page"),
"sZeroRecords" : gettext("Empty"),
"sInfo" : gettext("Records _START_ to _END_ of _TOTAL_"),
@ -30,6 +32,14 @@
"sPrevious" : gettext("Previous"),
}
};
gui.config.dataTableButtonsText = {
'new': '<span class="fa fa-pencil"></span> <span class="label-tbl-button">' + gettext('New') + '</span>',
'edit': '<span class="fa fa-edit"></span> <span class="label-tbl-button">' + gettext('Edit') + '</span>',
'delete': '<span class="fa fa-eraser"></span> <span class="label-tbl-button">' + gettext('Delete') + '</span>',
'refresh': '<span class="fa fa-refresh"></span> <span class="label-tbl-button">' + gettext('Refresh') + '</span>',
'xls': '<span class="fa fa-save"></span> <span class="label-tbl-button">' + gettext('Xls') + '</span>',
};
gui.table = function(title, table_id, options) {
if (options === undefined)
@ -158,19 +168,48 @@ GuiElement.prototype = {
},
});
},
// 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
// 3.- the DataTableTools 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
// 4.- the DataTableTools 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
// 4.- the DataTableTools 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
// 5.- the DataTableTools that raised the event
table : function(options) {
"use strict";
// Options (all are optionals)
// rowSelect: 'single' or 'multi'
// container: ID of the element that will hold this table (will be
// emptied)
// rowSelectFnc: function to invoke on row selection. receives 1 array -
// node : TR elements that were selected
// rowDeselectFnc: function to invoke on row deselection. receives 1
// array - node : TR elements that were selected
options = options || {};
gui.doLog('Composing table for ' + this.name);
var tableId = this.name + '-table';
var $this = this;
var $this = this; // Store this for child functions
// Empty cells transform
var renderEmptyCell = function(data) {
@ -212,21 +251,28 @@ GuiElement.prototype = {
return dict[data] || renderEmptyCell('');
};
};
this.rest.tableInfo({
success: function(data) {
var title = data.title;
var columns = [];
$.each(data.fields, function(index, value) {
for ( var v in value) {
var options = value[v];
var opts = value[v];
var column = {
mData : v,
};
column.sTitle = options.title;
column.sTitle = opts.title;
column.mRender = renderEmptyCell;
if (options.type !== undefined) {
switch(options.type) {
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 !== undefined && column.bVisible ) {
switch(opts.type) {
case 'date':
column.sType = 'date';
column.mRender = renderDate(api.tools.djangoFormat(get_format('SHORT_DATE_FORMAT')));
@ -243,31 +289,23 @@ GuiElement.prototype = {
column.mRender = renderTypeIcon;
break;
case 'icon':
if( options.icon !== undefined ) {
column.mRender = renderIcon(options.icon);
if( opts.icon !== undefined ) {
column.mRender = renderIcon(opts.icon);
}
break;
case 'dict':
if( options.dict !== undefined ) {
column.mRender = renderTextTransform(options.dict);
if( opts.dict !== undefined ) {
column.mRender = renderTextTransform(opts.dict);
}
break;
default:
column.sType = options.type;
column.sType = opts.type;
}
}
if (options.width)
column.sWidth = options.width;
if (options.visible !== undefined)
column.bVisible = options.visible;
if (options.sortable !== undefined)
column.bSortable = options.sortable;
if (options.searchable !== undefined)
column.bSearchable = options.searchable;
columns.push(column);
}
});
// Generate styles for responsibe table, just the name of fields
// Generate styles for responsible table, just the name of fields
var respStyles = [];
var counter = 0;
$.each(columns, function(col, value) {
@ -295,17 +333,18 @@ GuiElement.prototype = {
var btns = [];
if (options.buttons) {
var clickHandlerFor = function(handler, action) {
var handleFnc = handler || function(val, action, tbl, tbltools) {gui.doLog('Default handler called for ' + action + ': ' + JSON.stringify(val));};
return function(btn) {
var tblTools = this;
var table = $('#' + tableId).dataTable();
var val = this.fnGetSelectedData()[0];
setTimeout(function() {
handleFnc(val, action, table, tblTools);
}, 0);
};
};
// methods for buttons click
var editFnc = function() {
gui.doLog('Edit');
gui.doLog(this);
};
var deleteFnc = function() {
gui.doLog('Delete');
gui.doLog(this);
};
// What execute on refresh button push
var onRefresh = options.onRefresh || function(){};
@ -354,28 +393,37 @@ GuiElement.prototype = {
$.each(options.buttons, function(index, value) {
var btn;
switch (value) {
case 'new':
btn = {
"sExtends" : "text",
"sButtonText" : gui.config.dataTableButtonsText['new'],
"fnSelect" : deleteSelected,
"fnClick" : clickHandlerFor(options.onDelete, 'delete'),
"sButtonClass" : "disabled btn3d btn3d-tables"
};
break;
case 'edit':
btn = {
"sExtends" : "text",
"sButtonText" : gettext('Edit'),
"sButtonText" : gui.config.dataTableButtonsText['edit'],
"fnSelect" : editSelected,
"fnClick" : editFnc,
"fnClick" : clickHandlerFor(options.onEdit, 'edit'),
"sButtonClass" : "disabled btn3d btn3d-tables"
};
break;
case 'delete':
btn = {
"sExtends" : "text",
"sButtonText" : gettext('Delete'),
"sButtonText" : gui.config.dataTableButtonsText['delete'],
"fnSelect" : deleteSelected,
"fnClick" : deleteFnc,
"fnClick" : clickHandlerFor(options.onDelete, 'delete'),
"sButtonClass" : "disabled btn3d btn3d-tables"
};
break;
case 'refresh':
btn = {
"sExtends" : "text",
"sButtonText" : gettext('Refresh'),
"sButtonText" : gui.config.dataTableButtonsText['refresh'],
"fnClick" : refreshFnc,
"sButtonClass" : "btn3d-primary btn3d btn3d-tables"
};
@ -383,7 +431,7 @@ GuiElement.prototype = {
case 'xls':
btn = {
"sExtends" : "text",
"sButtonText" : 'xls',
"sButtonText" : gui.config.dataTableButtonsText['xls'],
"fnClick" : function(){
api.templates.get('spreadsheet', function(tmpl) {
var styles = { 'bold': 's21', };
@ -417,7 +465,6 @@ GuiElement.prototype = {
rows_count: rows.length,
rows: rows.join('\n')
};
// window.location.href = uri + base64(api.templates.evaluate(tmpl, ctx));
setTimeout( function() {
saveAs(new Blob([api.templates.evaluate(tmpl, ctx)],
{type: 'application/vnd.ms-excel'} ), title + '.xls');
@ -437,20 +484,29 @@ GuiElement.prototype = {
var oTableTools = {
"aButtons" : btns
};
// Type of row selection
if (options.rowSelect) {
oTableTools.sRowSelect = options.rowSelect;
}
if (options.onRowSelect) {
oTableTools.fnRowSelected = options.onRowSelect;
var rowSelectedFnc = options.onRowSelect;
oTableTools.fnRowSelected = function() {
rowSelectedFnc(this.fnGetSelectedData(), $('#' + tableId).dataTable(), this);
};
}
if (options.onRowDeselect) {
oTableTools.fnRowDeselected = options.onRowDeselect;
var rowDeselectedFnc = options.onRowDeselect;
oTableTools.fnRowDeselected = function() {
rowDeselectedFnc(this.fnGetSelectedData(), $('#' + tableId).dataTable(), this);
};
}
$('#' + tableId).dataTable({
"aaData" : data,
"aoColumns" : columns,
"oLanguage" : gui.dataTablesLanguage,
"oLanguage" : gui.config.dataTablesLanguage,
"oTableTools" : oTableTools,
// First is upper row,
// second row is lower
@ -462,7 +518,7 @@ GuiElement.prototype = {
api.tools.fix3dButtons('#' + tableId + '_wrapper .btn-group-3d');
// Fix form
//$('#' + tableId + '_filter input').addClass('form-control');
if (options.scroll !== undefined ) {
if (options.scrollToTable === true ) {
var tableTop = $('#' + tableId).offset().top;
$('html, body').scrollTop(tableTop);
}

View File

@ -1,392 +0,0 @@
//
// strftime
// github.com/samsonjs/strftime
// @_sjs
//
// Copyright 2010 - 2013 Sami Samhuri <sami@samhuri.net>
//
// MIT License
// http://sjs.mit-license.org
//
;(function() {
var namespace = api.tools;
var dayNames = [ gettext('Sunday'), gettext('Monday'), gettext('Tuesday'), gettext('Wednesday'),
gettext('Thursday'), gettext('Friday'), gettext('Saturday') ];
var monthNames = [ gettext('January'), gettext('February'), gettext('March'), gettext('April'), gettext('May'),
gettext('June'), gettext('July'), gettext('August'), gettext('September'), gettext('October'),
gettext('November'), gettext('December') ];
function initialsOf(arr) {
var res = [];
for ( var v in arr) {
res.push(arr[v].substr(0, 3));
}
return res;
}
var DefaultLocale = {
days : dayNames,
shortDays : initialsOf(dayNames),
months : monthNames,
shortMonths : initialsOf(monthNames),
AM : 'AM',
PM : 'PM',
am : 'am',
pm : 'pm',
};
// Added this to convert django format strings to c format string
// This is ofc, a "simplified" version, aimed to use date format used by
// DJANGO
namespace.djangoFormat = function(format) {
return format.replace(/./g, function(c) {
switch (c) {
case 'a':
case 'A':
return '%p';
case 'b':
case 'd':
case 'm':
case 'w':
case 'W':
case 'y':
case 'Y':
return '%' + c;
case 'c':
return '%FT%TZ';
case 'D':
return '%a';
case 'e':
return '%z';
case 'f':
return '%I:%M';
case 'F':
return '%F';
case 'h':
case 'g':
return '%I';
case 'H':
case 'G':
return '%H';
case 'i':
return '%M';
case 'I':
return ''; // daylight saving
case 'j':
return '%d';
case 'l':
return '%A';
case 'L':
return ''; // if it is leap year
case 'M':
return '%b';
case 'n':
return '%m';
case 'N':
return '%b';
case 'o':
return '%W'; // Not so sure, not important i thing anyway :-)
case 'O':
return '%z';
case 'P':
return '%R %p';
case 'r':
return '%a, %d %b %Y %T %z';
case 's':
return '%S';
case 'S':
return ''; // english ordinal suffix for day of month
case 't':
return ''; // number of days of specified month, not important
case 'T':
return '%Z';
case 'u':
return '0'; // microseconds
case 'U':
return ''; // Seconds since EPOCH, not used
case 'z':
return '%j';
case 'Z':
return 'z'; // Time zone offset in seconds, replaced by offset
// in ours/minutes :-)
default:
return c;
}
});
};
namespace.strftime = strftime;
function strftime(fmt, d, locale) {
return _strftime(fmt, d, locale);
}
// locale is optional
namespace.strftimeTZ = strftime.strftimeTZ = strftimeTZ;
function strftimeTZ(fmt, d, locale, timezone) {
if (typeof locale == 'number' && timezone === null) {
timezone = locale;
locale = undefined;
}
return _strftime(fmt, d, locale, {
timezone : timezone
});
}
namespace.strftimeUTC = strftime.strftimeUTC = strftimeUTC;
function strftimeUTC(fmt, d, locale) {
return _strftime(fmt, d, locale, {
utc : true
});
}
namespace.localizedStrftime = strftime.localizedStrftime = localizedStrftime;
function localizedStrftime(locale) {
return function(fmt, d, options) {
return strftime(fmt, d, locale, options);
};
}
// d, locale, and options are optional, but you can't leave
// holes in the argument list. If you pass options you have to pass
// in all the preceding args as well.
//
// options:
// - locale [object] an object with the same structure as DefaultLocale
// - timezone [number] timezone offset in minutes from GMT
function _strftime(fmt, d, locale, options) {
options = options || {};
// d and locale are optional so check if d is really the locale
if (d && !quacksLikeDate(d)) {
locale = d;
d = undefined;
}
d = d || new Date();
locale = locale || DefaultLocale;
locale.formats = locale.formats || {};
// Hang on to this Unix timestamp because we might mess with it directly
// below.
var timestamp = d.getTime();
if (options.utc || typeof options.timezone == 'number') {
d = dateToUTC(d);
}
if (typeof options.timezone == 'number') {
d = new Date(d.getTime() + (options.timezone * 60000));
}
// Most of the specifiers supported by C's strftime, and some from Ruby.
// Some other syntax extensions from Ruby are supported: %-, %_, and %0
// to pad with nothing, space, or zero (respectively).
return fmt.replace(/%([-_0]?.)/g, function(_, c) {
var mod, padding;
if (c.length == 2) {
mod = c[0];
// omit padding
if (mod == '-') {
padding = '';
}
// pad with space
else if (mod == '_') {
padding = ' ';
}
// pad with zero
else if (mod == '0') {
padding = '0';
} else {
// unrecognized, return the format
return _;
}
c = c[1];
}
switch (c) {
case 'A':
return locale.days[d.getDay()];
case 'a':
return locale.shortDays[d.getDay()];
case 'B':
return locale.months[d.getMonth()];
case 'b':
return locale.shortMonths[d.getMonth()];
case 'C':
return pad(Math.floor(d.getFullYear() / 100), padding);
case 'D':
return _strftime(locale.formats.D || '%m/%d/%y', d, locale);
case 'd':
return pad(d.getDate(), padding);
case 'e':
return d.getDate();
case 'F':
return _strftime(locale.formats.F || '%Y-%m-%d', d, locale);
case 'H':
return pad(d.getHours(), padding);
case 'h':
return locale.shortMonths[d.getMonth()];
case 'I':
return pad(hours12(d), padding);
case 'j':
var y = new Date(d.getFullYear(), 0, 1);
var day = Math.ceil((d.getTime() - y.getTime()) / (1000 * 60 * 60 * 24));
return pad(day, 3);
case 'k':
return pad(d.getHours(), padding === null ? ' ' : padding);
case 'L':
return pad(Math.floor(timestamp % 1000), 3);
case 'l':
return pad(hours12(d), padding === null ? ' ' : padding);
case 'M':
return pad(d.getMinutes(), padding);
case 'm':
return pad(d.getMonth() + 1, padding);
case 'n':
return '\n';
case 'o':
return String(d.getDate()) + ordinal(d.getDate());
case 'P':
return d.getHours() < 12 ? locale.am : locale.pm;
case 'p':
return d.getHours() < 12 ? locale.AM : locale.PM;
case 'R':
return _strftime(locale.formats.R || '%H:%M', d, locale);
case 'r':
return _strftime(locale.formats.r || '%I:%M:%S %p', d, locale);
case 'S':
return pad(d.getSeconds(), padding);
case 's':
return Math.floor(timestamp / 1000);
case 'T':
return _strftime(locale.formats.T || '%H:%M:%S', d, locale);
case 't':
return '\t';
case 'U':
return pad(weekNumber(d, 'sunday'), padding);
case 'u':
var dayu = d.getDay();
return dayu === 0 ? 7 : dayu; // 1 - 7, Monday is first day of the
// week
case 'v':
return _strftime(locale.formats.v || '%e-%b-%Y', d, locale);
case 'W':
return pad(weekNumber(d, 'monday'), padding);
case 'w':
return d.getDay(); // 0 - 6, Sunday is first day of the
// week
case 'Y':
return d.getFullYear();
case 'y':
var yy = String(d.getFullYear());
return yy.slice(yy.length - 2);
case 'Z':
if (options.utc) {
return "GMT";
} else {
var tz = d.toString().match(/\((\w+)\)/);
return tz && tz[1] || '';
}
break;
case 'z':
if (options.utc) {
return "+0000";
} else {
var off = typeof options.timezone == 'number' ? options.timezone : -d.getTimezoneOffset();
return (off < 0 ? '-' : '+') + pad(Math.abs(off / 60)) + pad(off % 60);
}
break;
default:
return c;
}
});
}
function dateToUTC(d) {
var msDelta = (d.getTimezoneOffset() || 0) * 60000;
return new Date(d.getTime() + msDelta);
}
var RequiredDateMethods = [ 'getTime', 'getTimezoneOffset', 'getDay', 'getDate', 'getMonth', 'getFullYear',
'getYear', 'getHours', 'getMinutes', 'getSeconds' ];
function quacksLikeDate(x) {
var i = 0, n = RequiredDateMethods.length;
for (i = 0; i < n; ++i) {
if (typeof x[RequiredDateMethods[i]] != 'function') {
return false;
}
}
return true;
}
// Default padding is '0' and default length is 2, both are optional.
function pad(n, padding, length) {
// pad(n, <length>)
if (typeof padding === 'number') {
length = padding;
padding = '0';
}
// Defaults handle pad(n) and pad(n, <padding>)
if (padding === null) {
padding = '0';
}
length = length || 2;
var s = String(n);
// padding may be an empty string, don't loop forever if it is
if (padding) {
while (s.length < length)
s = padding + s;
}
return s;
}
function hours12(d) {
var hour = d.getHours();
if (hour === 0)
hour = 12;
else if (hour > 12)
hour -= 12;
return hour;
}
// Get the ordinal suffix for a number: st, nd, rd, or th
function ordinal(n) {
var i = n % 10, ii = n % 100;
if ((ii >= 11 && ii <= 13) || i === 0 || i >= 4) {
return 'th';
}
switch (i) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
}
}
// firstWeekday: 'sunday' or 'monday', default is 'sunday'
//
// Pilfered & ported from Ruby's strftime implementation.
function weekNumber(d, firstWeekday) {
firstWeekday = firstWeekday || 'sunday';
// This works by shifting the weekday back by one day if we
// are treating Monday as the first day of the week.
var wday = d.getDay();
if (firstWeekday == 'monday') {
if (wday === 0) // Sunday
wday = 6;
else
wday--;
}
var firstDayOfYear = new Date(d.getFullYear(), 0, 1), yday = (d - firstDayOfYear) / 86400000, weekNum = (yday + 7 - wday) / 7;
return Math.floor(weekNum);
}
}());

View File

@ -55,10 +55,6 @@
<script src="{% get_static_prefix %}adm/js/Blob.js"></script>
<script src="{% get_static_prefix %}adm/js/FileSaver.js"></script>
<!-- strftime -->
<script src="{% get_static_prefix %}adm/js/strftime.js"></script>
<!-- <script src="{% get_static_prefix %}adm/js/ZeroClipboard.js"></script> -->
<script src="{% get_static_prefix %}adm/js/dataTables.bootstrap.js"></script>
@ -80,18 +76,22 @@
<!-- First all api related stuff -->
<script src="{% get_static_prefix %}adm/js/api.js"></script>
<!-- utilities attached to api -->
<script src="{% get_static_prefix %}adm/js/api-tools.js"></script>
<!-- templates related, inserts itself into api -->
<script src="{% get_static_prefix %}adm/js/templates.js"></script>
<!-- export to xls, inserts itself into api -->
<script src="{% get_static_prefix %}adm/js/spreadsheet.js"></script>
<script src="{% get_static_prefix %}adm/js/api-spreadsheet.js"></script>
<!-- utilities attached to api -->
<script src="{% get_static_prefix %}adm/js/tools.js"></script>
<script src="{% get_static_prefix %}adm/js/gui.js"></script>
<script src="{% get_static_prefix %}adm/js/gui-fields.js"></script>
<!-- user interface management -->
<script src="{% get_static_prefix %}adm/js/gui-elements.js"></script>
<script>