diff --git a/server/src/uds/REST/methods/accounts.py b/server/src/uds/REST/methods/accounts.py
new file mode 100644
index 000000000..80e5ca1fc
--- /dev/null
+++ b/server/src/uds/REST/methods/accounts.py
@@ -0,0 +1,75 @@
+# -*- 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.
+
+'''
+@itemor: Adolfo Gómez, dkmaster at dkmon dot com
+'''
+from __future__ import unicode_literals
+
+from django.utils.translation import ugettext_lazy as _, ugettext
+from uds.models import Account, AccountUsage
+from uds.core.util import permissions
+
+from uds.REST.model import ModelHandler
+
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+# Enclosed methods under /item path
+
+
+class Accounts(ModelHandler):
+ '''
+ Processes REST requests about calendars
+ '''
+ model = Account
+ detail = {'usage': AccountUsage}
+
+ save_fields = ['name', 'comments', 'tags']
+
+ table_title = _('Accounts')
+ table_fields = [
+ {'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}},
+ {'comments': {'title': _('Comments')}},
+ {'tags': {'title': _('tags'), 'visible': False}},
+ ]
+
+ def item_as_dict(self, calendar):
+ return {
+ 'id': calendar.uuid,
+ 'name': calendar.name,
+ 'tags': [tag.tag for tag in calendar.tags.all()],
+ 'comments': calendar.comments,
+ 'permission': permissions.getEffectivePermission(self._user, calendar)
+ }
+
+ def getGui(self, type_):
+ return self.addDefaultFields([], ['name', 'comments', 'tags'])
diff --git a/server/src/uds/REST/methods/services_pools.py b/server/src/uds/REST/methods/services_pools.py
index 4954359b9..0116e7223 100644
--- a/server/src/uds/REST/methods/services_pools.py
+++ b/server/src/uds/REST/methods/services_pools.py
@@ -33,7 +33,7 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext, ugettext_lazy as _
-from uds.models import DeployedService, OSManager, Service, Image, ServicesPoolGroup
+from uds.models import DeployedService, OSManager, Service, Image, ServicesPoolGroup, Account
from uds.models.CalendarAction import CALENDAR_ACTION_INITIAL, CALENDAR_ACTION_MAX, CALENDAR_ACTION_CACHE_L1, CALENDAR_ACTION_CACHE_L2, CALENDAR_ACTION_PUBLISH
from uds.core.ui.images import DEFAULT_THUMB_BASE64
from uds.core.util.State import State
@@ -69,7 +69,21 @@ class ServicesPools(ModelHandler):
'actions': ActionsCalendars
}
- save_fields = ['name', 'comments', 'tags', 'service_id', 'osmanager_id', 'image_id', 'servicesPoolGroup_id', 'initial_srvs', 'cache_l1_srvs', 'cache_l2_srvs', 'max_srvs', 'show_transports']
+ save_fields = [
+ 'name',
+ 'comments',
+ 'tags',
+ 'service_id',
+ 'osmanager_id',
+ 'image_id',
+ 'account_id',
+ 'servicesPoolGroup_id',
+ 'initial_srvs',
+ 'cache_l1_srvs',
+ 'cache_l2_srvs',
+ 'max_srvs',
+ 'show_transports'
+ ]
remove_fields = ['osmanager_id', 'service_id']
table_title = _('Service Pools')
@@ -116,10 +130,12 @@ class ServicesPools(ModelHandler):
'comments': item.comments,
'state': state,
'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64,
+ 'account': item.account.name if item.account is not None else '',
'service_id': item.service.uuid,
'provider_id': item.service.provider.uuid,
'image_id': item.image.uuid if item.image is not None else None,
'servicesPoolGroup_id': poolGroupId,
+ 'account_id': item.account.uuid if item.account is not None else None,
'pool_group_name': poolGroupName,
'pool_group_thumb': poolGroupThumb,
'initial_srvs': item.initial_srvs,
@@ -165,13 +181,20 @@ class ServicesPools(ModelHandler):
'type': gui.InputField.CHOICE_TYPE,
'rdonly': True,
'order': 101,
+ }, {
+ 'name': 'account_id',
+ 'values': [gui.choiceItem(-1, '')] + gui.sortedChoices([gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]),
+ 'label': ugettext('Account'),
+ 'tooltip': ugettext('Account associated to this service pool'),
+ 'type': gui.InputField.CHOICE_TYPE,
+ 'order': 102,
}, {
'name': 'image_id',
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]),
'label': ugettext('Associated Image'),
'tooltip': ugettext('Image assocciated with this service'),
'type': gui.InputField.IMAGECHOICE_TYPE,
- 'order': 102,
+ 'order': 105,
'tab': ugettext('Display'),
}, {
'name': 'servicesPoolGroup_id',
@@ -179,7 +202,7 @@ class ServicesPools(ModelHandler):
'label': ugettext('Pool group'),
'tooltip': ugettext('Pool group for this pool (for pool clasify on display)'),
'type': gui.InputField.IMAGECHOICE_TYPE,
- 'order': 103,
+ 'order': 106,
'tab': ugettext('Display'),
}, {
'name': 'initial_srvs',
@@ -270,6 +293,18 @@ class ServicesPools(ModelHandler):
# If max < initial or cache_1 or cache_l2
fields['max_srvs'] = max((int(fields['initial_srvs']), int(fields['cache_l1_srvs']), int(fields['max_srvs'])))
+
+ accountId = fields['account_id']
+ fields['account_id'] = None
+ logger.debug('Account id: {}'.format(accountId))
+
+ if accountId != '-1':
+ try:
+ fields['account_id'] = Account.objects.get(uuid=processUuid(accountId)).id
+ except Exception:
+ logger.exception('Getting account ID')
+
+
imgId = fields['image_id']
fields['image_id'] = None
logger.debug('Image id: {}'.format(imgId))
diff --git a/server/src/uds/migrations/0024_auto_20161010_0753.py b/server/src/uds/migrations/0024_auto_20170117_0845.py
similarity index 95%
rename from server/src/uds/migrations/0024_auto_20161010_0753.py
rename to server/src/uds/migrations/0024_auto_20170117_0845.py
index a736338c5..03cb627a2 100644
--- a/server/src/uds/migrations/0024_auto_20161010_0753.py
+++ b/server/src/uds/migrations/0024_auto_20170117_0845.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.10 on 2016-10-10 07:53
+# Generated by Django 1.10.5 on 2017-01-17 08:45
from __future__ import unicode_literals
import datetime
@@ -21,6 +21,7 @@ class Migration(migrations.Migration):
('uuid', models.CharField(default=None, max_length=50, null=True, unique=True)),
('name', models.CharField(db_index=True, max_length=128)),
('comments', models.CharField(max_length=256)),
+ ('tags', models.ManyToManyField(to='uds.Tag')),
],
options={
'db_table': 'uds_accounts',
diff --git a/server/src/uds/models/Account.py b/server/src/uds/models/Account.py
index 7f0003026..642ffedbb 100644
--- a/server/src/uds/models/Account.py
+++ b/server/src/uds/models/Account.py
@@ -31,11 +31,12 @@
from __future__ import unicode_literals
-__updated__ = '2016-09-21'
+__updated__ = '2017-01-17'
from django.db import models
from uds.models.UUIDModel import UUIDModel
+from uds.models.Tag import TaggingMixin
from uds.models.Util import getSqlDatetime
from django.db.models import signals
@@ -44,11 +45,9 @@ import logging
logger = logging.getLogger(__name__)
-class Account(UUIDModel):
+class Account(UUIDModel, TaggingMixin):
'''
Account storing on DB model
- This is intended for small images (i will limit them to 128x128), so storing at db is fine
-
'''
name = models.CharField(max_length=128, unique=False, db_index=True)
comments = models.CharField(max_length=256)
diff --git a/server/src/uds/models/UserService.py b/server/src/uds/models/UserService.py
index 93a4beb13..6356819ab 100644
--- a/server/src/uds/models/UserService.py
+++ b/server/src/uds/models/UserService.py
@@ -57,7 +57,7 @@ import six
import pickle
import logging
-__updated__ = '2017-01-12'
+__updated__ = '2017-01-17'
logger = logging.getLogger(__name__)
@@ -368,7 +368,7 @@ class UserService(UUIDModel):
# 1.- If do not have any account associated, do nothing
# 2.- If called but already accounting, do nothing
# 3.- If called and not accounting, start accounting
- if self.deployed_service.account is None or hasattr(self, 'accounting'):
+ if self.deployed_service.account is None or hasattr(self, 'accounting'): # accounting comes from AccountUsage, and is a OneToOneRelation with UserService
return
self.deployed_service.account.startUsageAccounting(self)
diff --git a/server/src/uds/static/adm/js/api.coffee b/server/src/uds/static/adm/js/api.coffee
index 4225694fb..ff002039b 100644
--- a/server/src/uds/static/adm/js/api.coffee
+++ b/server/src/uds/static/adm/js/api.coffee
@@ -444,6 +444,7 @@ api.sPoolGroups = new BasicModelRest("gallery/servicespoolgroups")
api.system = new BasicModelRest("system")
api.reports = new BasicModelRest("reports") # Not fully used, but basic usage is common
api.calendars = new BasicModelRest("calendars")
+api.accounts = new BasicModelRest("accounts")
# In fact, reports do not have any type
api.reports.types = (success_fnc, fail_fnc) ->
diff --git a/server/src/uds/static/adm/js/gui-d-account.coffee b/server/src/uds/static/adm/js/gui-d-account.coffee
new file mode 100644
index 000000000..ecfe1b1c4
--- /dev/null
+++ b/server/src/uds/static/adm/js/gui-d-account.coffee
@@ -0,0 +1,439 @@
+# jshint strict: true
+gui.accounts = new GuiElement(api.accounts, "accounts")
+gui.accounts.link = (event) ->
+ "use strict"
+
+ # Button definition to trigger "Test" action
+ testButton = testButton:
+ text: gettext("Test")
+ css: "btn-info"
+
+
+ # Clears the log of the detail, in this case, the log of "users"
+ # Memory saver :-)
+ detailLogTable = null
+ clearDetailLog = ->
+ if detailLogTable?
+ $tbl = $(detailLogTable).dataTable()
+ $tbl.fnClearTable()
+ $tbl.fnDestroy()
+ detailLogTable = null
+ $("#users-log-placeholder").empty()
+ return
+
+
+ # Clears the details
+ # Memory saver :-)
+ prevTables = []
+ clearDetails = ->
+ clearDetailLog()
+
+ $.each prevTables, (undefined_, tbl) ->
+ $tbl = $(tbl).dataTable()
+ $tbl.fnClearTable()
+ $tbl.fnDestroy()
+ return
+
+ $("#users-placeholder").empty()
+ $("#groups-placeholder").empty()
+ $("#logs-placeholder").empty()
+ $("#detail-placeholder").addClass "hidden"
+ prevTables = []
+ return
+
+
+ # Search button event generator for user/group
+ searchForm = (parentModalId, type, id, title, searchLabel, resultsLabel) ->
+ errorModal = gui.failRequestModalFnc(gettext("Search error"))
+ srcSelector = parentModalId + " input[name=\"name\"]"
+ $(parentModalId + " .button-search").on "click", ->
+ api.templates.get "search", (tmpl) -> # Get form template
+ modalId = gui.launchModal(title, api.templates.evaluate(tmpl,
+ search_label: searchLabel
+ results_label: resultsLabel
+ ),
+ actionButton: ""
+ )
+ $searchInput = $(modalId + " input[name=\"search\"]")
+ $select = $(modalId + " select[name=\"results\"]")
+ $searchButton = $(modalId + " .button-do-search")
+ $saveButton = $(modalId + " .button-accept")
+ $searchInput.val $(srcSelector).val()
+ $saveButton.on "click", ->
+ value = $select.val()
+ if value
+ $(srcSelector).val value
+ $(modalId).modal "hide"
+ return
+
+ $searchButton.on "click", ->
+ $searchButton.addClass "disabled"
+ term = $searchInput.val()
+ api.accounts.search id, type, term, ((data) ->
+ $searchButton.removeClass "disabled"
+ $select.empty()
+ gui.doLog data
+ $.each data, (undefined_, value) ->
+ $select.append ""
+ return
+
+ return
+ ), (jqXHR, textStatus, errorThrown) ->
+ $searchButton.removeClass "disabled"
+ errorModal jqXHR, textStatus, errorThrown
+ return
+
+ return
+
+ $(modalId + " form").submit (event) ->
+ event.preventDefault()
+ $searchButton.click()
+ return
+
+ $searchButton.click() if $searchInput.val() isnt ""
+ return
+
+ return
+
+ return
+
+ api.templates.get "accounts", (tmpl) ->
+ gui.clearWorkspace()
+ gui.appendToWorkspace api.templates.evaluate(tmpl,
+ auths: "auths-placeholder"
+ auths_info: "auths-info-placeholder"
+ users: "users-placeholder"
+ users_log: "users-log-placeholder"
+ groups: "groups-placeholder"
+ logs: "logs-placeholder"
+ )
+ gui.setLinksEvents()
+
+ # Append tabs click events
+ $(".bottom_tabs").on "click", (event) ->
+ gui.doLog event.target
+ setTimeout (->
+ $($(event.target).attr("href") + " span.fa-refresh").click()
+ return
+ ), 10
+ return
+
+ tableId = gui.accounts.table(
+ icon: 'accounts'
+ container: "auths-placeholder"
+ rowSelect: "multi"
+ buttons: [
+ "new"
+ "edit"
+ "delete"
+ "xls"
+ "permissions"
+ ]
+
+ onFoundUuid: (item) ->
+ # Invoked if our table has found a "desirable" item (uuid)
+ if gui.lookup2Uuid?
+ type = gui.lookup2Uuid[0]
+ gui.lookupUuid = gui.lookup2Uuid.substr(1)
+ gui.lookup2Uuid = null
+ setTimeout( () ->
+ if type == 'g'
+ $('a[href="#groups-placeholder"]').tab('show')
+ $("#groups-placeholder span.fa-refresh").click()
+ else
+ $('a[href="#users-placeholder_tab"]').tab('show')
+ $("#users-placeholder_tab span.fa-refresh").click()
+ , 500)
+
+ onRefresh: (tbl) ->
+ gui.doLog 'Refresh called for accounts'
+ clearDetails()
+ return
+
+ onRowDeselect: (deselected, dtable) ->
+ clearDetails()
+ return
+
+ onRowSelect: (selected) ->
+ clearDetails()
+
+ if selected.length > 1
+ return
+
+ # We can have lots of users, so memory can grow up rapidly if we do not keep thins clean
+ # To do so, we empty previous table contents before storing new table contents
+ # Anyway, TabletTools will keep "leaking" memory, but we can handle a little "leak" that will be fixed as soon as we change the section
+ $("#detail-placeholder").removeClass "hidden"
+ $('#detail-placeholder a[href="#auths-info-placeholder"]').tab('show')
+
+ gui.tools.blockUI()
+
+ # Load provider "info"
+ gui.methods.typedShow gui.accounts, selected[0], '#auths-info-placeholder .well', gettext('Error accessing data')
+
+ id = selected[0].id
+ type = gui.accounts.types[selected[0].type]
+ gui.doLog "Type", type
+ user = new GuiElement(api.accounts.detail(id, "users", { permission: selected[0].permission }), "users")
+ group = new GuiElement(api.accounts.detail(id, "groups", { permission: selected[0].permission }), "groups")
+ grpTable = group.table(
+ icon: 'groups'
+ container: "groups-placeholder"
+ doNotLoadData: true
+ rowSelect: "multi"
+ buttons: [
+ "new"
+ "edit"
+ "delete"
+ "xls"
+ ]
+ onLoad: (k) ->
+ gui.tools.unblockUI()
+ return
+
+ onEdit: (value, event, table, refreshFnc) ->
+ exec = (groups_all) ->
+ gui.tools.blockUI()
+ api.templates.get "group", (tmpl) -> # Get form template
+ group.rest.item value.id, (item) -> # Get item to edit
+ # Creates modal
+ modalId = gui.launchModal(gettext("Edit group") + " " + item.name + "", api.templates.evaluate(tmpl,
+ id: item.id
+ type: item.type
+ meta_if_any: item.meta_if_any
+ groupname: item.name
+ groupname_label: type.groupNameLabel
+ comments: item.comments
+ state: item.state
+ external: type.isExternal
+ canSearchGroups: type.canSearchGroups
+ groups: item.groups
+ groups_all: groups_all
+ ))
+ gui.tools.applyCustoms modalId
+ gui.tools.unblockUI()
+ $(modalId + " .button-accept").click ->
+ fields = gui.forms.read(modalId)
+ gui.doLog "Fields", fields
+ group.rest.save fields, ((data) -> # Success on put
+ $(modalId).modal "hide"
+ refreshFnc()
+ gui.notify gettext("Group saved"), "success"
+ return
+ ), gui.failRequestModalFnc("Error saving group", true)
+ return
+
+ return
+
+ return
+
+ return
+
+ if value.type is "meta"
+
+ # Meta will get all groups
+ group.rest.overview (groups) ->
+ exec groups
+ return
+
+ else
+ exec()
+ return
+
+ onNew: (t, table, refreshFnc) ->
+ exec = (groups_all) ->
+ gui.tools.blockUI()
+ api.templates.get "group", (tmpl) -> # Get form template
+ # Creates modal
+ if t is "meta"
+ title = gettext("New meta group")
+ else
+ title = gettext("New group")
+ modalId = gui.launchModal(title, api.templates.evaluate(tmpl,
+ type: t
+ groupname_label: type.groupNameLabel
+ external: type.isExternal
+ canSearchGroups: type.canSearchGroups
+ groups: []
+ groups_all: groups_all
+ ))
+ gui.tools.unblockUI()
+ gui.tools.applyCustoms modalId
+ searchForm modalId, "group", id, gettext("Search groups"), gettext("Group"), gettext("Groups found") # Enable search button click, if it exist ofc
+ $(modalId + " .button-accept").click ->
+ fields = gui.forms.read(modalId)
+ gui.doLog "Fields", fields
+ group.rest.create fields, ((data) -> # Success on put
+ $(modalId).modal "hide"
+ refreshFnc()
+ gui.notify gettext("Group saved"), "success"
+ return
+ ), gui.failRequestModalFnc(gettext("Group saving error"), true)
+ return
+
+ return
+
+ return
+
+ if t is "meta"
+ # Meta will get all groups
+ group.rest.overview (groups) ->
+ exec groups
+ return
+
+ else
+ exec()
+ return
+
+ onDelete: gui.methods.del(group, gettext("Delete group"), gettext("Group deletion error"))
+ )
+ tmpLogTable = null
+
+ # New button will only be shown on accounts that can create new users
+ usrButtons = [
+ "edit"
+ "delete"
+ "xls"
+ ]
+ usrButtons = ["new"].concat(usrButtons) if type.canCreateUsers # New is first button
+ usrTable = user.table(
+ icon: 'users'
+ container: "users-placeholder"
+ doNotLoadData: true
+ rowSelect: "multi"
+ onRowSelect: (uselected) ->
+ gui.doLog 'User row selected ', uselected
+ gui.tools.blockUI()
+ uId = uselected[0].id
+ clearDetailLog()
+ tmpLogTable = user.logTable(uId,
+ container: "users-log-placeholder"
+ onLoad: ->
+ detailLogTable = tmpLogTable
+ gui.tools.unblockUI()
+ return
+ )
+ return
+
+ onRowDeselect: ->
+ clearDetailLog()
+ return
+
+ buttons: usrButtons
+ deferedRender: true # Use defered rendering for users, this table can be "huge"
+ scrollToTable: false
+ onLoad: (k) ->
+ gui.tools.unblockUI()
+ return
+
+ onRefresh: ->
+ gui.doLog "Refreshing"
+ clearDetailLog()
+ return
+
+ onEdit: (value, event, table, refreshFnc) ->
+ password = "#æð~¬ŋ@æß”¢€~½¬@#~þ¬@|" # Garbage for password (to detect change)
+ gui.tools.blockUI()
+ api.templates.get "user", (tmpl) -> # Get form template
+ group.rest.overview (groups) -> # Get groups
+ user.rest.item value.id, (item) -> # Get item to edit
+
+ # Creates modal
+ modalId = gui.launchModal(gettext("Edit user") + " " + value.name + "", api.templates.evaluate(tmpl,
+ id: item.id
+ username: item.name
+ username_label: type.userNameLabel
+ realname: item.real_name
+ comments: item.comments
+ state: item.state
+ staff_member: item.staff_member
+ is_admin: item.is_admin
+ needs_password: type.needsPassword
+ password: (if type.needsPassword then password else undefined)
+ password_label: type.passwordLabel
+ groups_all: groups
+ groups: item.groups
+ external: type.isExternal
+ canSearchUsers: type.canSearchUsers
+ ))
+ gui.tools.applyCustoms modalId
+ gui.tools.unblockUI()
+ $(modalId + " .button-accept").click ->
+ fields = gui.forms.read(modalId)
+
+ # If needs password, and password has changed
+ gui.doLog "passwords", type.needsPassword, password, fields.password
+ delete fields.password if fields.password is password if type.needsPassword
+ gui.doLog "Fields", fields
+ user.rest.save fields, ((data) -> # Success on put
+ $(modalId).modal "hide"
+ refreshFnc()
+ gui.notify gettext("User saved"), "success"
+ return
+ ), gui.failRequestModalFnc(gettext("User saving error"), true)
+ return
+
+ return
+
+ return
+
+ return
+
+ return
+
+ onNew: (undefined_, table, refreshFnc) ->
+ gui.tools.blockUI()
+ api.templates.get "user", (tmpl) -> # Get form template
+ group.rest.overview (groups) -> # Get groups
+ # Creates modal
+ modalId = gui.launchModal(gettext("New user"), api.templates.evaluate(tmpl,
+ username_label: type.userNameLabel
+ needs_password: type.needsPassword
+ password_label: type.passwordLabel
+ groups_all: groups
+ groups: []
+ external: type.isExternal
+ canSearchUsers: type.canSearchUsers
+ ))
+ gui.tools.applyCustoms modalId
+ gui.tools.unblockUI()
+ searchForm modalId, "user", id, gettext("Search users"), gettext("User"), gettext("Users found") # Enable search button click, if it exist ofc
+ $(modalId + " .button-accept").click ->
+ fields = gui.forms.read(modalId)
+
+ # If needs password, and password has changed
+ gui.doLog "Fields", fields
+ user.rest.create fields, ((data) -> # Success on put
+ $(modalId).modal "hide"
+ refreshFnc()
+ gui.notify gettext("User saved"), "success"
+ return
+ ), gui.failRequestModalFnc(gettext("User saving error"), true)
+ return
+
+ return
+
+ return
+
+ return
+
+ onDelete: gui.methods.del(user, gettext("Delete user"), gettext("User deletion error"))
+ )
+ logTable = gui.accounts.logTable(id,
+ container: "logs-placeholder"
+ doNotLoadData: true
+ )
+
+ # So we can destroy the tables beforing adding new ones
+ prevTables.push grpTable
+ prevTables.push usrTable
+ prevTables.push logTable
+ false
+
+ onNew: gui.methods.typedNew(gui.accounts, gettext("New authenticator"), gettext("Authenticator creation error"), testButton)
+ onEdit: gui.methods.typedEdit(gui.accounts, gettext("Edit authenticator"), gettext("Authenticator saving error"), testButton)
+ onDelete: gui.methods.del(gui.accounts, gettext("Delete authenticator"), gettext("Authenticator deletion error"))
+ )
+ return
+
+ false
diff --git a/server/src/uds/templates/uds/admin/index.html b/server/src/uds/templates/uds/admin/index.html
index 1a799a7fb..d018227c4 100644
--- a/server/src/uds/templates/uds/admin/index.html
+++ b/server/src/uds/templates/uds/admin/index.html
@@ -127,6 +127,8 @@
+
+