-- Copyright 2008 Steven Barth -- Copyright 2017 Matthias Schiffer -- 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, pkg) 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 i18n = setmetatable({ i18n = renderer.i18n }, { __index = renderer.i18n(pkg) }) setfenv(func, setmetatable({}, {__index = function(tbl, key) return _M[key] or i18n[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 self.package = 'gluon-web' 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, self.package) 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, ...) key = tostring(key) if self.keys[key] then return end self.keys[key] = true val = val or key table.insert(self.entry_list, { key = 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