gluon/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua
Matthias Schiffer ec532b95cf
gluon-web: extend ListValue with optional and unset values
If a value is unset or optional, an empty choice is added to the selection.
This empty choice will be marked as invalid if the value is not optional.

This is properly supported for the 'select' widget only for now, and not
for 'radio'.
2018-01-31 17:08:21 +01:00

493 lines
8.7 KiB
Lua

-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
-- Licensed to the public under the Apache License 2.0.
module("gluon.web.model", package.seeall)
local util = require("gluon.web.util")
local fs = require("nixio.fs")
local datatypes = require("gluon.web.model.datatypes")
local dispatcher = require("gluon.web.dispatcher")
local class = util.class
local instanceof = util.instanceof
FORM_NODATA = 0
FORM_VALID = 1
FORM_INVALID = -1
-- Loads a model from given file, creating an environment and returns it
function load(name, renderer)
local modeldir = util.libpath() .. "/model/"
if not fs.access(modeldir..name..".lua") then
error("Model '" .. name .. "' not found!")
end
local func = assert(loadfile(modeldir..name..".lua"))
local env = {
translate=renderer.translate,
translatef=renderer.translatef,
}
setfenv(func, setmetatable(env, {__index =
function(tbl, key)
return _M[key] or _G[key]
end
}))
local models = { func() }
for k, model in ipairs(models) do
if not instanceof(model, Node) then
error("model definition returned an invalid model object")
end
model.index = k
end
return models
end
local function parse_datatype(code)
local match, arg, arg2
match, arg, arg2 = code:match('^([^%(]+)%(([^,]+),([^%)]+)%)$')
if match then
return datatypes[match], {arg, arg2}
end
match, arg = code:match('^([^%(]+)%(([^%)]+)%)$')
if match then
return datatypes[match], {arg}
end
return datatypes[code], {}
end
local function verify_datatype(dt, value)
if dt then
local c, args = parse_datatype(dt)
assert(c, "Invalid datatype")
return c(value, unpack(args))
end
return true
end
Node = class()
function Node:__init__(title, description, name)
self.children = {}
self.title = title or ""
self.description = description or ""
self.name = name
self.index = nil
self.parent = nil
end
function Node:append(obj)
table.insert(self.children, obj)
obj.index = #self.children
obj.parent = self
end
function Node:id_suffix()
return self.name or (self.index and tostring(self.index)) or '_'
end
function Node:id()
local prefix = self.parent and self.parent:id() or "id"
return prefix.."."..self:id_suffix()
end
function Node:parse(http)
for _, child in ipairs(self.children) do
child:parse(http)
end
end
function Node:render(renderer, scope)
if self.template then
local env = setmetatable({
self = self,
id = self:id(),
scope = scope,
}, {__index = scope})
renderer.render(self.template, env)
end
end
function Node:render_children(renderer, scope)
for _, node in ipairs(self.children) do
node:render(renderer, scope)
end
end
function Node:resolve_depends()
local updated = false
for _, node in ipairs(self.children) do
update = updated or node:resolve_depends()
end
return updated
end
function Node:handle()
for _, node in ipairs(self.children) do
node:handle()
end
end
Template = class(Node)
function Template:__init__(template)
Node.__init__(self)
self.template = template
end
Form = class(Node)
function Form:__init__(...)
Node.__init__(self, ...)
self.template = "model/form"
end
function Form:submitstate(http)
return http:getenv("REQUEST_METHOD") == "POST" and http:formvalue(self:id()) ~= nil
end
function Form:parse(http)
if not self:submitstate(http) then
self.state = FORM_NODATA
return
end
Node.parse(self, http)
while self:resolve_depends() do end
for _, s in ipairs(self.children) do
for _, v in ipairs(s.children) do
if v.state == FORM_INVALID then
self.state = FORM_INVALID
return
end
end
end
self.state = FORM_VALID
end
function Form:handle()
if self.state == FORM_VALID then
Node.handle(self)
self:write()
end
end
function Form:write()
end
function Form:section(t, ...)
assert(instanceof(t, Section), "class must be a descendent of Section")
local obj = t(...)
self:append(obj)
return obj
end
Section = class(Node)
function Section:__init__(...)
Node.__init__(self, ...)
self.fields = {}
self.template = "model/section"
end
function Section:option(t, option, title, description, ...)
assert(instanceof(t, AbstractValue), "class must be a descendant of AbstractValue")
local obj = t(title, description, option, ...)
self:append(obj)
self.fields[option] = obj
return obj
end
AbstractValue = class(Node)
function AbstractValue:__init__(option, ...)
Node.__init__(self, option, ...)
self.deps = {}
self.default = nil
self.size = nil
self.optional = false
self.template = "model/valuewrapper"
self.state = FORM_NODATA
end
function AbstractValue:depends(field, value)
local deps
if instanceof(field, Node) then
deps = { [field] = value }
else
deps = field
end
table.insert(self.deps, deps)
end
function AbstractValue:deplist(section, deplist)
local deps = {}
for _, d in ipairs(deplist or self.deps) do
local a = {}
for k, v in pairs(d) do
a[k:id()] = v
end
table.insert(deps, a)
end
if next(deps) then
return deps
end
end
function AbstractValue:defaultvalue()
return self.default
end
function AbstractValue:formvalue(http)
return http:formvalue(self:id())
end
function AbstractValue:cfgvalue()
if self.state == FORM_NODATA then
return self:defaultvalue()
else
return self.data
end
end
function AbstractValue:add_error(type, msg)
self.error = msg or type
if type == "invalid" then
self.tag_invalid = true
elseif type == "missing" then
self.tag_missing = true
end
self.state = FORM_INVALID
end
function AbstractValue:reset()
self.error = nil
self.tag_invalid = nil
self.tag_missing = nil
self.data = nil
self.state = FORM_NODATA
end
function AbstractValue:parse(http)
self.data = self:formvalue(http)
local ok, err = self:validate()
if not ok then
if type(self.data) ~= "string" or #self.data > 0 then
self:add_error("invalid", err)
else
self:add_error("missing", err)
end
return
end
self.state = FORM_VALID
end
function AbstractValue:resolve_depends()
if self.state == FORM_NODATA or #self.deps == 0 then
return false
end
for _, d in ipairs(self.deps) do
local valid = true
for k, v in pairs(d) do
if k.state ~= FORM_VALID or k.data ~= v then
valid = false
break
end
end
if valid then return false end
end
self:reset()
return true
end
function AbstractValue:validate()
if self.data and verify_datatype(self.datatype, self.data) then
return true
end
if type(self.data) == "string" and #self.data == 0 then
self.data = nil
end
if self.data == nil then
return self.optional
end
return false
end
function AbstractValue:handle()
if self.state == FORM_VALID then
self:write(self.data)
end
end
function AbstractValue:write(value)
end
Value = class(AbstractValue)
function Value:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/value"
end
Flag = class(AbstractValue)
function Flag:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/fvalue"
self.default = false
end
function Flag:formvalue(http)
return http:formvalue(self:id()) ~= nil
end
function Flag:validate()
return true
end
ListValue = class(AbstractValue)
function ListValue:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/lvalue"
self.size = 1
self.widget = "select"
self.keys = {}
self.entry_list = {}
end
function ListValue:value(key, val, ...)
if self.keys[key] then
return
end
val = val or key
self.keys[key] = true
table.insert(self.entry_list, {
key = tostring(key),
value = tostring(val),
deps = {...},
})
end
function ListValue:entries()
local ret = {unpack(self.entry_list)}
if self:cfgvalue() == nil or self.optional then
table.insert(ret, 1, {
key = '',
value = '',
deps = {},
})
end
return ret
end
function ListValue:validate()
if self.keys[self.data] then
return true
end
if type(self.data) == "string" and #self.data == 0 then
self.data = nil
end
if self.data == nil then
return self.optional
end
return false
end
DynamicList = class(AbstractValue)
function DynamicList:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/dynlist"
end
function DynamicList:defaultvalue()
local value = self.default
if type(value) == "table" then
return value
else
return { value }
end
end
function DynamicList:formvalue(http)
return http:formvaluetable(self:id())
end
function DynamicList:validate()
if self.data == nil then
self.data = {}
end
if #self.data == 0 then
return self.optional
end
for _, v in ipairs(self.data) do
if not verify_datatype(self.datatype, v) then
return false
end
end
return true
end
TextValue = class(AbstractValue)
function TextValue:__init__(...)
AbstractValue.__init__(self, ...)
self.subtemplate = "model/tvalue"
end