Luci, Add a section populated by dropdown (which is populated from config file)

Title sez goal, 1000 words below;
I have stripped this down as much as I can, to simplify debugging - which I have failed at

To get the picture above, I manually populated the config file, which loads OK;

I seem to missing / not comprehending something.

What I am seeing (firefox console), on page load:
many instances (SyntaxError: "Unhandled token "value""):

    raise https://127.0.0.1/luci-static/resources/luci.js?v=git-19.196.26199-784e764:84
    compile https://127.0.0.1/luci-static/resources/validation.js:17
    __init__ https://127.0.0.1/luci-static/resources/validation.js:1
    ClassConstructor https://127.0.0.1/luci-static/resources/luci.js?v=git-19.196.26199-784e764:11
    create https://127.0.0.1/luci-static/resources/validation.js:12
    cbi_validate_field https://127.0.0.1/luci-static/resources/cbi.js?v=git-19.196.26199-784e764:58
    cbi_init https://127.0.0.1/luci-static/resources/cbi.js?v=git-19.196.26199-784e764:46
    setupDOM https://127.0.0.1/luci-static/resources/luci.js?v=git-19.196.26199-784e764:clock1130:

many instances (SyntaxError: "Unhandled token "button""):

    raise https://127.0.0.1/luci-static/resources/luci.js?v=git-19.196.26199-784e764:84
    compile https://127.0.0.1/luci-static/resources/validation.js:17
    __init__ https://127.0.0.1/luci-static/resources/validation.js:1
    ClassConstructor https://127.0.0.1/luci-static/resources/luci.js?v=git-19.196.26199-784e764:11
    create https://127.0.0.1/luci-static/resources/validation.js:12
    cbi_validate_field https://127.0.0.1/luci-static/resources/cbi.js?v=git-19.196.26199-784e764:58
    cbi_init https://127.0.0.1/luci-static/resources/cbi.js?v=git-19.196.26199-784e764:46
    setupDOM https://127.0.0.1/luci-static/resources/luci.js?v=git-19.196.26199-784e764:113

When I select an example and press OK nothing but page reload happens.

Files:
/etc/config/example (starts empty until section created)
/etc/config/example_sections (used to populate dropbox for examples to select, selected contents to /etc/config/example when added):

config example 'Select'
        option name             'Please select example'

config example 'example_1'
        option name             'example_1'
        option description      'Example_1'
        option product_url      'www.rossco.org'
        option payment_id       ''

config example 'example_2'
        option name             'example_2'
        option description      'Example_2'
        option product_url      'www.rossco.org'
        option payment_id       ''

/usr/lib/lua/luci/controller/example.lua:

-- luci.controller.name of application directory.name of application(resolves to application.lua)
module("luci.controller.example", package.seeall)

function index()
        if not nixio.fs.access("/etc/config/example") then
                return
        end

        local page

        -- this adds the top level tab and defaults to the first sub-tab, also it is set to position 30
        entry({"admin", "system", "Example"}, firstchild(), "Example", 2).dependent=false
        entry({"admin", "system", "Example", "example"}, cbi("example_tab"), "Registration", 1)
end

/usr/lib/lua/luci/model/cbi/example_tab.lua:

-- to see call tree for functions (to understand calling order)
-- m.message = m.message .. debug.traceback()
local util = require("luci.util")
local uci = require "luci.model.uci".cursor()
local fs = require "nixio.fs"
local logfile = "/var/log/vtdaemon.log"
local licenseFile = "example"
local licensesFile = "example_sections"
local daemonCmd = "vtdaemon -D file logfile -S "
local a, b, c, btn1, btn2, btn3, btn4, btn5, m,  m1, s, s1

local select_options = { }

m = Map(licenseFile, translate("Manage Applications / Licences"), "")
-- licenseFile is the config file in /etc/config
--m.message = ""

s = m:section(TypedSection, "example", "Select Example to Add")
-- example is the section called "example" in config file
s.addremove = true
s.sectionhead = "Application"
s.template = "cbi/tblsection"
s.template_addremove = "example-select-input-add"
s.add_select_options = select_options

uci:load(licensesFile)
uci:foreach( licensesFile, "example",
  function(section)
    select_options[section['.name']] = section['name'] or section['description']
  end
)

function s.create(self, name)
  -- sections are initially created from /etc/config/example_sections
  -- by: m = Map("example", translate("Manage Licences"), "")
  -- and s.create is only called when a new section is created from dropdown listbox
  -- section is populated with defaults from /etc/config/example_sections

  if name and not name:match("[^a-zA-Z0-9_]") then
    if name ~= "Select" then
      -- Don`t add section if "Select" (used only for dropdown section default menu entry)
      uci:section( licenseFile, "example", name, uci:get_all( licensesFile, name ) )
      uci:save(licenseFile)
    end
  else
    m.message = "invalid"
    self.invalid_cts = true
  end
end

function s.validate(self, section)
  if section == "Select" then
    return nil
  end
  return section
end

function s.remove(self, section)
  -- Remove application first
  local cmd = daemonCmd .. section .. " -U"
  cmdret = luci.sys.exec( cmd )
  return TypedSection.remove(self, section)
end
function s.remove(self, section)
  -- Remove application first
  local cmd = daemonCmd .. section .. " -U"
  cmdret = luci.sys.exec( cmd )
  return TypedSection.remove(self, section)
end

a = s:option(Value, "description", translate("Description")); a.optional=false; a.rmempty = false;
a.readonly = true
function a.cfgvalue(self, section)
  local val = AbstractValue.cfgvalue(self, section)
  return val
end

b = s:option(Value, "product_url", translate("Application Support Site")); b.optional=false; b.rmempty = false;
b.readonly = true
function b.cfgvalue(self, section)
  local val = AbstractValue.cfgvalue(self, section)
  return val
end

c = s:option(Value, "payment_id", translate("Purchase Receipt ID#")); c.optional=false; c.rmempty = false;
function c.cfgvalue(self, section)
  local val = AbstractValue.cfgvalue(self, section)
  return val or ""
end
function c:validate(value)
  if ( value ~= nil ) and ( string.len(value) > 63 ) then
    return nil, translate("Receipt maximum length = 63")
  end
  return value
end

btn = s:option( Button, "_btn1", translate("Register"), "")
btn.value = "Register"
function btn.write(self, section, value)
  local cmd = daemonCmd .. section .. " -R"
  cmdret = luci.sys.exec( cmd )
  return reload
end

btn = s:option( Button, "_btn2", translate("Install"), "")
btn.value = "Install"
function btn.write(self, section, value)
  local cmd = daemonCmd .. section .. " -G"
  cmdret = luci.sys.exec( cmd )
  return reload
end

btn = s:option( Button, "_btn3", translate("Verify"), "")
btn.value = "Verify"
function btn.write(self, section, value)
  local cmd = daemonCmd .. section .. " -V"
  cmdret = luci.sys.exec( cmd )
  return reload
end

btn = s:option( Button, "_btn4", translate("Remove"), "")
btn.value = "Remove"
function btn.write(self, section, value)
  local cmd = daemonCmd .. section .. " -U"
  cmdret = luci.sys.exec( cmd )
  return reload
end

return m, m1, m2

/usr/lib/lua/luci/view/example-select-input-add.htm (custom template for populating dropdown):

<div class="cbi-section-create">
        <select class="cbi-section-create-name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.select">
        <%- for k, v in luci.util.kspairs(self.add_select_options) do %>
                <option value="<%=k%>"><%=luci.util.pcdata(v)%></option>
        <% end -%>
        </select>
        <input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" title="<%:Add%>" />
</div>

At my wits end on this. Any suggestions?

Thanks;
Bill

Please try changing your dropdown name to cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%> and see if it works then.

Thanks Jow;

That may be part of the problem, but not root cause. Here's the form with empty config (/etc/config/example):

Turns out that section text (Name, Description, so on) are what is displayed when the config is empty (a, b,c, btn's) and the unhandled token errors (value, button) are because they are empty. Clicking "Add" (assumption) doesn't work because of these errors.

It seems that I should be doing this:
a) Only display section header text (Name, Description, Application Support...) when there is actually something in section.
b) Only display textboxes and buttons for corresponding fields in config - already working

Another thing I don't understand is why what appears to be the header field, which is really empty textboxes and buttons (with associated token errors) displays when there is no corresponding config entry.

I need to know how to do these things:
a) create header (Name, Description...), where the element horiz size / spacing matches the textbox and button spacing below.
b) Not attempt to render / display anything in section, including the header unless there is section data to display. Which can only happen after section add button has been clicked.
c) Still need the "Please select example" dropdown to display.

already tried "s.rmempty = true" to no avail.

Regards;
Bill

Partially solved (can now add and remove sections, see "doesn't work"):

      -- create and populate section - doesn't work
      --uci:section( licenseFile, "example", name, uci:get_all( licensesFile, name ) )
      -- create section
      uci:section( licenseFile, "example", name )
      local options = uci:get_all(licensesFile, name)
      for k, v in pairs(options) do
        uci:set(licenseFile, name, k, v)
      end
      uci:commit(licenseFile)

I am still getting (SyntaxError: "Unhandled token "value"") and SyntaxError: "Unhandled token "button"" each time the page loads.

which appears to be some sort of order / timing problem with references to textbox and button variables before they are initialized.

Latest files:
/etc/config/example (starts empty until section created)
/etc/config/example_sections (used to populate dropbox for examples to select, selected contents to /etc/config/example when added):

config example 'Select'
        option name             'Please select example'

config example 'example_1'
        option name             'example_1'
        option description      'Example_1'
        option product_url      'www.rossco.org'
        option payment_id       ''

config example 'example_2'
        option name             'example_2'
        option description      'Example_2'
        option product_url      'www.rossco.org'
        option payment_id       ''

/usr/lib/lua/luci/controller/example.lua:

-- luci.controller.name of application directory.name of application(resolves to application.lua)
module("luci.controller.example", package.seeall)

function index()
        if not nixio.fs.access("/etc/config/example_sections") then
                return
        end
        -- no config create an empty one
        if not nixio.fs.access("/etc/config/example") then
               nixio.fs.writefile("/etc/config/example", "")
        end

        local page

        -- this adds the top level tab and defaults to the first sub-tab, also it is set to position 30
        entry({"admin", "system", "Example"}, firstchild(), "Example", 2).dependent=false
        entry({"admin", "system", "Example", "example"}, cbi("example_tab"), "Registration", 1)
end

/usr/lib/lua/luci/model/cbi/example_tab.lua:

local util = require("luci.util")
local uci = require "luci.model.uci".cursor()
local fs = require "nixio.fs"
local logfile = "/var/log/vtdaemon.log"
local licenseFile = "example"
local licensesFile = "example_sections"
local daemonCmd = "vtdaemon -D file logfile -S "
local a, b, c, btn1, btn2, btn3, btn4, m, s

local select_options = { }
uci:load(licensesFile)
uci:foreach( licensesFile, "example",
  function(section)
    select_options[section['.name']] = section['name'] or section['description']
  end
)

m = Map(licenseFile, translate("Manage Applications / Licences"), "")
-- licenseFile is the config file in /etc/config
-- to see call tree for functions (to understand calling order)
-- m.message = m.message .. debug.traceback()
--m.message = ""

s = m:section(TypedSection, "example", "Select Example to Add")
-- example is the section called "example" in config file
s.addremove = true
s.template = "cbi/tblsection"
s.template_addremove = "example-select-input-add"
s.add_select_options = select_options

function s.create(self, name)
  -- sections are initially created from /etc/config/example_sections
  -- by: m = Map("example", translate("Manage Licences"), "")
  -- and s.create is only called when a new section is created from dropdown listbox
  -- section is populated with defaults from /etc/config/example_sections
  -- Don`t add section for select entry (used only for dropdown section default menu entry)
  if name ~= "Please select example" then
    if name and not name:match("[^a-zA-Z0-9_]") then
      -- create and populate section - doesn't work
      --uci:section( licenseFile, "example", name, uci:get_all( licensesFile, name ) )
      -- create section
      uci:section( licenseFile, "example", name )
      local options = uci:get_all(licensesFile, name)
      for k, v in pairs(options) do
        uci:set(licenseFile, name, k, v)
      end
      uci:commit(licenseFile)
    else
      m.message = "invalid"
      self.invalid_cts = true
    end
  end
end

function s.validate(self, section)
  if section == "Select" then
    return nil
  end
  return section
end

function s.remove(self, section)
  -- Remove application first
  local cmd = daemonCmd .. section .. " -U"
  cmdret = luci.sys.exec( cmd )
  TypedSection.remove(self, section)
  uci:commit(licenseFile)
end

a = s:option(Value, "description", translate("Description"))
a.optional = false
a.rmempty = false
a.readonly = true
function a.cfgvalue(self, section)
  local val = AbstractValue.cfgvalue(self, section)
    return val
end

b = s:option(Value, "product_url", translate("Application Support Site"))
b.optional=false
b.rmempty = false
b.readonly = true
function b.cfgvalue(self, section)
  local val = AbstractValue.cfgvalue(self, section)
  return val
end

c = s:option(Value, "payment_id", translate("Purchase Receipt ID#"))
c.optional=false
c.rmempty = false
function c.cfgvalue(self, section)
  local val = AbstractValue.cfgvalue(self, section)
  return val or ""
end
function c:validate(value)
  if ( value ~= nil ) and ( string.len(value) > 63 ) then
    return nil, translate("Receipt maximum length = 63")
  end
  return value
end

btn1 = s:option( Button, "_btn1", translate("Register"), "")
btn1.value = "Register"

function btn1.write(self, section, value)
  local cmd = daemonCmd .. section .. " -R"
  cmdret = luci.sys.exec( cmd )
  return reload
end

btn2 = s:option( Button, "_btn2", translate("Install"), "")
btn2.value = "Install"
function btn2.write(self, section, value)
  local cmd = daemonCmd .. section .. " -G"
  cmdret = luci.sys.exec( cmd )
  return reload
end

btn3 = s:option( Button, "_btn3", translate("Verify"), "")
btn3.value = "Verify"
function btn3.write(self, section, value)
  local cmd = daemonCmd .. section .. " -V"
  cmdret = luci.sys.exec( cmd )
  return reload
end

btn4 = s:option( Button, "_btn4", translate("Remove"), "")
btn4.value = "Remove"
function btn4.write(self, section, value)
  local cmd = daemonCmd .. section .. " -U"
  cmdret = luci.sys.exec( cmd )
  return reload
end

return m

/usr/lib/lua/luci/view/example-select-input-add.htm (custom template for populating dropdown):

<div class="cbi-section-create">
        <!-- <select class="cbi-section-create-name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.select"> -->
        <select class="cbi-section-create-name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>">
        <%- for k, v in luci.util.kspairs(self.add_select_options) do %>
                <option value="<%=k%>"><%=luci.util.pcdata(v)%></option>
        <% end -%>
        </select>
        <input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" title="<%:Add%>" />
</div>

Questions:

why does "uci:section( licenseFile, "example", name, uci:get_all( licensesFile, name ) )" not work? - copy "name" section from licensesFile to licenseFile

It there anything I can do to get rid of the "Unhandled token" warnings? They are annoying (tells me I am not comprehending something) but do not interfere with functionality.

Thanks;
Bill

I installed your example files on my test system and added both example_1 and example_2 sections. The resulting /etc/config/example looked like this:

config example 'example_1'
	option name 'example_1'
	option description 'Example_1'
	option product_url 'www.rossco.org'

config example 'example_2'
	option name 'example_2'
	option description 'Example_2'
	option product_url 'www.rossco.org'

So to me it seems as if it worked? I also added an option extra foo to example_2 of /etc/config/example_sections, deleted and readded example_2 via the ui and that extra option was copied too. Can you elaborate some more on what exactly is not working with the section copy?

As for the JavaScript errors - they're unrelated to your code and an actual bug/quirk in LuCI, will take care of that.

Thanks Jow;

As you have seen. code is working. I was curious
that:
uci:section( licenseFile, "example", name, uci:get_all( licensesFile, name ) ) doesn't work.

replaced with:

      uci:section( licenseFile, "example", name )
      local options = uci:get_all(licensesFile, name)
      for k, v in pairs(options) do
        uci:set(licenseFile, name, k, v)
      end

which does work.

Good to hear that JS errors not mine. hard knocks has taught me that ignoring anomalies can only lead to grief, in addition to not fully understanding code.

Ah, sorry. I was only skimming your post and didn't pay close attention. I'll check why the former call didn't work.

FYI, the "point" of having a dropdown (consisting of special applications to install) populated from a config file is so, as new products become available, the config file is replaced with latest offerings as part of init transaction with license server.

You can't be smart without also being curious:)

www.rossco.org is what I am doing...

So the JS errors have been addressed with https://github.com/openwrt/luci/commit/13e9e3e9e8633c7a54fe5fec1481e9df62594982 and backported to the 19.07 and 18.06 branches.

Add:
Okay, so the reason why uci:section( licenseFile, "example", name, uci:get_all( licensesFile, name ) ) does not work is the following:

  • uci:get_all() returns a table of all options including additional metadata fields like .index, .type or .anonymous.
  • If you pass that table to uci:section() it will fail with Invalid argument since fields starting with a dot are not valid.

The following code replicates the error condition on the command line:

root@jj:~# lua -e 'js = require "luci.jsonc"; uci = require "luci.model.uci"; s = uci:get_all("example_sections", "example_1"); print(js.stringif
y(s, true)); ok, err = uci:section("example", "example", "example_1", s); print(ok, err)'
{
   ".name": "example_1",
   ".type": "example",
   "name": "example_1",
   ".anonymous": false,
   "description": "Example_1",
   "product_url": "www.rossco.org"
 }
false	Invalid argument
root@jj:~# 

I am not sure if I'd consider this a bug and if I should change section() to simply ignore invalid options passed to it.

I recommend not changing sections. Ignoring all invalid options will lead to hard to debug issues. Just ignoring options starting with "." may be a reasonable compromise, if all get_all "invalid" options start with "." Another approach may be to add a "real" flag, as an optional get_all parm n to eliminate non-config file fields.

Really, a minor issue...

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.