Module:Version: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
>Illviljan
mNo edit summary
m (48 revisions imported)
 
(19 intermediate revisions by 5 users not shown)
Line 1: Line 1:
local getArgs = require('Module:Arguments').getArgs
-------------------------------------------------------------------------------
local util = require('Module:Util')
--
local cargo = mw.ext.cargo
--                                Module:Version
--
-- This module implements Template:Version, Template:Version history list, and
-- Template:Timeline of items
-------------------------------------------------------------------------------


local string_format = string.format
require('Module:No globals')
local table_concat = table.concat
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_item_util -- Lazy load require('Module:Item util')


local mw_html = mw.html
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Version')


local mw_language = mw.getLanguage('en')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Version/config/sandbox') or mw.loadData('Module:Version/config')


local p = {}
local i18n = cfg.i18n
 
local date_format = 'd F Y H:i:s'
 
local i18n = {
    timeline = {
        version = 'Version',
    },
}


-- ---------------------------------------------------------------------
-- ---------------------------------------------------------------------
-- Helper
-- Helper functions
-- ---------------------------------------------------------------------
-- ---------------------------------------------------------------------


h = {}
local h = {}


function h.cargo_query(tpl_args)
function h.date(value, args)
     --[[
     --[[
     Returns a Cargo query of all the results, even if there are more
     Format dates in correct and useable form.
    results than the maximum query limit. It also adds popular fields.  
      
      
     tpl_args should include these keys:
     Parameters
     tpl_args.tables
     ----------
     tpl_args.fields
     value : String, required
     tpl_args.q_*
        Date
     args : Table
        Table with extra formatting args.
      
      
     ]]
     ]]
      
      
     local tables = util.string.split(tpl_args.tables, ', ')
     local args = args or {}
    local fields = util.string.split(tpl_args.fields, ', ')
      
      
     -- Parse query arguments
     -- List of allowed extra arguments:
     local query_limit = 5000
     local arg_list = {
    local query = {
        format = {
        -- Workaround: fix duplicates
            default = 'F j, Y H:i:s',
        groupBy='versions._pageID',
            cargo  = 'Y-m-d H:i:s',
        limit = query_limit,
            no_time = 'F j, Y',
         offset = 0,
         },
     }
     }
     for key, value in pairs(tpl_args) do
 
        if string.sub(key, 0, 2) == 'q_' then
     local lang = mw.getContentLanguage()
            query[string.sub(key, 3)] = value
    local date_format = arg_list['format']['default']
        end
    local timestamp = lang:formatDate(date_format, value)
   
    -- If the time is 00:00:00 then assume that the time isn't defined:
    if lang:formatDate('H:i:s', timestamp) == '00:00:00' then  
        date_format = arg_list['format']['no_time']
     end
     end
      
      
     -- Add commonly used fields:
     -- Add the extra arguments:
    fields_base = {
     for i,v in pairs(args) do
        '_pageID',
         if i == 'format' then
        '_pageName'
             date_format = arg_list[i][v]          
    }
     for _, tbl in ipairs(tables) do
         for _, fld in ipairs(fields_base) do
             fields[#fields+1] = string.format('%s.%s', tbl, fld)
         end
         end
     end
     end
      
      
     -- Query cargo table. If there are too many results then repeat,
     -- Return the final timestamp format:
    -- offset, query and add the remaining results:
     local out
     results = {}     
     if value ~= nil then
     repeat
         out = lang:formatDate(date_format, timestamp)
local result = mw.ext.cargo.query(
    end
            table.concat(tables, ', '),
            table.concat(fields, ', '),
            query
         )
query.offset = query.offset + #result
for _,v in ipairs(result) do
results[#results + 1] = v
end
until #result < query_limit
      
      
     return results
     return out
end
end


 
function h.validate_version(value)
 
-- ---------------------------------------------------------------------
-- Template: Version
-- ---------------------------------------------------------------------
 
 
function validate_version(value)
     if value == nil then
     if value == nil then
         return value
         return value
    else
        return util.cast.version(value, {return_type='string'})
     end
     end
    return m_util.cast.version(value, {return_type='string'})
end
end


function show_date(args)
function h.show_date(args)
     return function(tpl_args, frame)
     return function(targs)
         local version = tpl_args[args.key]
         local version = targs[args.key]
         local date = tpl_args[string.format('%s_date', args.key)]
         local date = targs[string.format('%s_date', args.key)]
         if version and date then
         if version and date then
             date = mw_language:formatDate(date_format, date)
             date = h.date(date) or ''
             if args.key == 'before' then
             if args.key == 'before' then
                 return string_format('← [[Version %s|%s]]<br>%s', version, version, date)
                 return string.format(i18n.show_date.before, version, version, date)
             elseif args.key == 'after' then
             elseif args.key == 'after' then
                 return string_format('[[Version %s|%s]] →<br>%s', version, version, date)
                 return string.format(i18n.show_date.after, version, version, date)
             end
             end
         else
         else
Line 117: Line 100:
end
end


-- ----------------------------------------------------------------------------
-- Cargo tables
-- ----------------------------------------------------------------------------
local tables = {}


local version_map = {
tables.versions ={
     table = 'versions',
     table = 'versions',
     fields = {
     fields = {
Line 124: Line 112:
             field = 'version',
             field = 'version',
             type = 'String',
             type = 'String',
             validate = validate_version,
             func = h.validate_version,
         },
         },
         patchdate = {
         patchdate = {
             field = 'release_date',
             field = 'release_date',
             type = 'Datetime',
             type = 'Datetime',
             validate = tostring,
             func = tostring,
         },
         },
         major_part = {
         major_part = {
Line 148: Line 136:
         },
         },
         before = {
         before = {
             field = 'before',
             field = 'previous',
             type = 'String',
             type = 'String',
             validate = validate_version,
             func = h.validate_version,
             show = show_date{key='before'},
             show = h.show_date{key='before'},
         },
         },
         after = {
         after = {
             field = 'after',
             field = 'after',
             type = 'String',
             type = 'String',
             validate = validate_version,
             func = h.validate_version,
             show = show_date{key='after'},
             show = h.show_date{key='after'},
         },
         },
     },
     },
}
}


p.table_versions = util.cargo.declare_factory{data=version_map}
-- ----------------------------------------------------------------------------
 
-- Main functions
p.version = function(frame)
-- ----------------------------------------------------------------------------
    local args = getArgs(frame, {parentFirst = true})
    local frame = util.misc.get_frame(frame)


local function _version(args)
     --[[
     --[[
     = p.version({
     Creates a version succession box and stores the data in a cargo table
         before = '2.4.1a',
   
    Example:
    p.version{
         before = '2.4.1',
         patch = '2.4.1b',
         patch = '2.4.1b',
         patchdate = 'October 18, 2016',
         patchdate = 'October 18, 2016',
         after = '2.4.2',
         after = '2.4.2',
     })
     }
     --]]
     --]]
     for k, data in pairs(version_map.fields) do
 
         if data.validate ~= nil then
    -- Unpack args and validate
             args[k] = data.validate(args[k])
     for k, arg_def in pairs(tables.versions.fields) do
         if arg_def.func ~= nil then
             args[k] = arg_def.func(args[k])
         end
         end
     end
     end
     if not args.patch or not args.patchdate then
     if not args.patch or not args.patchdate then
         error('Arguments "patch" and "patchdate" are required')
         error(i18n.version.required_args)
     end
     end
      
      
     local version_parts = util.cast.version(args.patch, {return_type='table'})
     local version_parts = m_util.cast.version(args.patch, {return_type='table'})
     args.major_part = tonumber(version_parts[1])
     args.major_part = tonumber(version_parts[1])
     args.minor_part = tonumber(version_parts[2])
     args.minor_part = tonumber(version_parts[2])
Line 194: Line 185:
     end
     end


     -- Check and set 'before' and 'after' args
     -- Validate 'before' and 'after' versions and query their release dates
     local edge_names = {'before', 'after'}
     for _, key in ipairs({'before', 'after'}) do
    for _, key in ipairs(edge_names) do
         local version_number = args[key]
         local v = args[key]
         if version_number then
         if v then
             local results = m_cargo.query(
             local results = cargo.query(
                 {'versions'},
                 'versions',  
                 {'versions.release_date=date'},
                 'versions.release_date',  
                 {
                 {
                     where=string.format('version="%s"', v),
                     where = string.format('versions.version="%s"', version_number)
                    -- Cargo bug work around
                    groupBy='versions._pageID',
                 }
                 }
             )
             )
             if #results == 1 then
             if #results == 1 then
                 args[string.format('%s_date', key)] = results[1]['versions.release_date']
                 args[string.format('%s_date', key)] = results[1].date
             elseif #results > 1 then
             elseif #results > 1 then
                 error('There are multiple versions with the same name')
                 error(i18n.version.multiple_versions)
             end
             end
         end
         end
     end
     end
 
   
 
     -- Store cargo data
     -- Set Cargo data
     local data = {
     local _properties = {
         _table = tables.versions.table,
         _table = version_map.table,
     }
     }
     for key, data in pairs(version_map.fields) do
     for k, v in pairs(tables.versions.fields) do
         if args[key] ~= nil then
         if args[k] ~= nil then
             _properties[data.field] = args[key]
             data[v.field] = args[k]
       end
       end
     end
     end
      
     m_cargo.store(data)
    util.cargo.store(frame, _properties)


    mw.getCurrentFrame():expandTemplate{
        title = 'Template:Version/cargo/versions/attach'
    }


     -- Generate output
     -- Generate output
    -- todo: rework it somehow
     local release_date = h.date(args.patchdate)
     local release_date = mw_language:formatDate(date_format, args.patchdate)
     local tbl = mw.html.create('table')
 
     local tbl = mw_html.create('table')
     tbl
     tbl
         :addClass('wikitable successionbox')
         :addClass('wikitable successionbox')
Line 240: Line 227:
             :tag('th')
             :tag('th')
                 :attr('colspan', 3)
                 :attr('colspan', 3)
                 :wikitext('[[Version history|Version History]]')
                 :wikitext(i18n.version.header)
                 :done()
                 :done()
             :done()
             :done()
Line 246: Line 233:
             :tag('td')
             :tag('td')
                 :cssText('width: 30%')
                 :cssText('width: 30%')
                 :wikitext(version_map.fields.before.show(args, frame))
                 :wikitext(tables.versions.fields.before.show(args))
                 :done()
                 :done()
             :tag('td')
             :tag('td')
                 :cssText('width: 40%')
                 :cssText('width: 40%')
                 :wikitext(string_format('<b>%s</b><br>%s', args.patch, release_date))
                 :wikitext(string.format('<b>%s</b><br>%s', args.patch, release_date))
                 :done()
                 :done()
             :tag('td')
             :tag('td')
                 :cssText('width: 30%')
                 :cssText('width: 30%')
                 :wikitext(version_map.fields.after.show(args, frame))
                 :wikitext(tables.versions.fields.after.show(args))


    local cats = {
     return tostring(tbl) .. m_util.misc.add_category({i18n.categories.versions})
        'Versions',
    }
 
     return tostring(tbl) .. util.misc.add_category(cats)
end
end


-----
local function _timeline(args)
    --[[
    Creates a version timeline and optionally lists items added to the game for each version
   
    Examples:
    p.timeline{
        where = 'versions.major_part = 0 AND versions.minor_part < 9',
    }
   
    p.timeline{
        list_items = true
        where = 'items.class_id = "DivinationCard"',
    }
    --]]


p.version_declare = function(frame)
     local tables = {'versions'}
     -- local args = getArgs(frame, {parentFirst = true})
     local fields = {
     local frame = util.misc.get_frame(frame)
        'versions.version',
 
         'versions.release_date',
    local props = {
         _table = 'Versions',
     }
     }
--    for i, _ in pairs(version_map) do
--        props[i] = _.datatype
--    end
    for i = 1, #temp_map_for_cargo do
        local v = temp_map_for_cargo[i]
        local _ = version_map[v]
        props[v] = _.datatype
    end
    --mw.logObject(props)
    return util.cargo.declare(frame, props)
end
-----
------------------------------------------------------------------------------------------------------
-- Template: Version history list
p.version_history_list = function(frame)
    local args = getArgs(frame, {parentFirst = true})
    local frame = util.misc.get_frame(frame)
    -- = p.version_history_list({conditions='[[Is version::~1*||~2*]]'})
    -- = p.version_history_list({conditions='[[Is version::~0.9*]]'})
    -- = p.version_history_list({conditions='[[Is version::~0.5*]]'})
    if args.conditions then
        args.conditions = args.conditions .. '[[Has release date::+]]'
    else
        args.conditions = '[[Is version::+]][[Has release date::+]]'
    end
     local query = {
     local query = {
         args.conditions,
         orderBy = 'versions.major_part DESC, versions.minor_part DESC, versions.patch_part DESC, versions.revision_part DESC'
        '?Is version',
        '?Has release date',
        sort = 'Has release date, Is version',
        order = 'desc, desc',
        link = 'none',
        offset = 0,
     }
     }
    args.list_items = m_util.cast.boolean(args.list_items)
    if args.list_items then
        m_item_util = m_item_util or require('Module:Item util')
        table.insert(tables, 'items')
        fields = m_util.table.merge(fields, {'items._pageName', 'items.name'})
        query.join = 'versions.version=items.release_version'
        query.where = 'items.release_version IS NOT NULL'
        query.orderBy = query.orderBy .. ', items.name ASC'


    local results = {}
         -- Namespace condition
    repeat
         -- This is mainly to prevent items from user pages or other testing pages
         local result = util.smw.query(query, frame)
         -- from being returned in the query results.
         local length = #result
         if args.namespaces ~= 'any' then
         query.offset = query.offset + length
             local namespaces = m_util.cast.table(args.namespaces, {callback=m_util.cast.number})
 
            if #namespaces > 0 then
         for i = 1, length do
                namespaces = table.concat(namespaces, ',')
             results[#results + 1] = result[i]
             else
        end
                 namespaces = m_item_util.get_item_namespaces{format = 'list'}
    until length < 1000
 
    local out = {}
    local last_minor_version, current_list
 
    for i = 1, #results do
        local result = results[i]
        local date = result['Has release date']
        local version = result['Is version']
 
        local v = util.cast.version(result['Is version'])
        local minor_version = table_concat({v[1], v[2], v[3]}, '.') -- todo: rework it
 
        if minor_version ~= last_minor_version then
             if current_list ~= nil then
                 out[#out + 1] = tostring(current_list)
             end
             end
 
             query.where = string.format('%s AND items._pageNamespace IN (%s)', query.where, namespaces)
             out[#out + 1] = string_format('===Version %s===', minor_version)
            current_list = mw_html.create('ul')
        end
 
        current_list:tag('li'):wikitext(string_format('%s &ndash; [[Version %s]]', date, version))
 
        -- save the last list
        if i == #results and current_list ~= nil then
            out[#out + 1] = tostring(current_list)
         end
         end
        last_minor_version = minor_version
     end
     end
 
     if args.where then
     return table_concat(out, '\n')
        -- m_util.table.merge rebuilds the table, which removes empty values
end
        -- TODO: Use a better function than m_util.table.merge
 
         query.where = table.concat(m_util.table.merge({query.where, args.where}), ' AND ')
-----
 
p.version_history_list_2 = function(frame)
    local args = getArgs(frame, {parentFirst = true})
    local frame = util.misc.get_frame(frame)
 
    -- = p.version_history_list({conditions='[[Is version::~1*||~2*]]'})
    -- = p.version_history_list({conditions='[[Is version::~0.9*]]'})
    -- = p.version_history_list({conditions='[[Is version::~0.5*]]'})
 
    if args.conditions then
         args.conditions = args.conditions .. '[[Has release date::+]]'
    else
        args.conditions = '[[Is version::+]] [[Has release date::+]]'
     end
     end
 
     local results = m_cargo.query(tables, fields, query)
    local query = {
      
        args.conditions,
        '?Is version',
        '?Has release date',
        '?Has major version part',
        '?Has minor version part',
        '?Has patch version part',
--        '?Has revision version part',
        sort = 'Has major version part, Has minor version part, Has patch version part, Is version',
        order = 'desc, desc, desc, desc',
        link = 'none',
        offset = 0,
    }
 
     local results = {}
    repeat
        local result = util.smw.query(query, frame)
        local length = #result
        query.offset = query.offset + length
 
        for i = 1, length do
            results[#results + 1] = result[i]
        end
     until length < 1000
 
--    mw.logObject(results)
 
     local out = {}
     local out = {}
     local last_minor_version, current_list
     local last_main_version  
 
    for i = 1, #results do
        local result = results[i]
        local date = result['Has release date']
        local version = result['Is version']
 
        local patch_version = string_format('%s.%s.%s',
            result['Has major version part'], result['Has minor version part'], result['Has patch version part'])
 
        if patch_version ~= last_minor_version then
            if current_list ~= nil then
                out[#out + 1] = tostring(current_list)
            end
 
            out[#out + 1] = string_format('===Version %s===', patch_version)
            current_list = mw_html.create('ul')
        end
 
        current_list:tag('li'):wikitext(string_format('%s &ndash; [[Version %s]]', date, version))
 
        -- save the last list
        if i == #results and current_list ~= nil then
            out[#out + 1] = tostring(current_list)
        end
 
        last_minor_version = patch_version
    end
 
    return table_concat(out, '\n')
end
 
function p.timeline(frame)
    --[[
    Adds a timeline when each result from the smw query was released in
    the game.
TODO:
    * Support for different result formats ie table, plain text etc.
    * Make sure it works with items (and areas?) once item2 uses cargo.
   
    Examples:
    = p.timeline{
        tables = 'versions',
        fields = 'versions.version, versions.release_date',
        q_where = 'versions.version <> ""',
        q_orderBy = 'versions.version DESC, versions.release_date ASC'
    }
   
    ]]
   
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    local frame = util.misc.get_frame(frame)
    -- Query results
    local results = h.cargo_query(tpl_args)
   
local out = {}
local last_main_version  
     local last_minor_version
     local last_minor_version
     local current_version
     local current_version
     local result_list
     local list
 
   
for i, result in ipairs(results) do  
    -- Loop through all the results from the query
release_version = result[tpl_args.tables .. '.release_version']
    for i, row in ipairs(results) do
if release_version == nil then
        local release_version = row['versions.version']
release_version = result[tpl_args.tables .. '.version']
        local v = m_util.cast.version(release_version)
end
        local version_h2 = table.concat({v[1], v[2]}, '.')
        if release_version ~= last_minor_version then
local v = util.cast.version(release_version)
            if version_h2 ~= last_main_version then  
local version_h2 = table.concat({v[1], v[2]}, '.')
                if current_version ~= nil then
                    out[#out + 1] = tostring(current_version)
if release_version ~= last_minor_version then
                end
if version_h2 ~= last_main_version then  
               
if current_version ~= nil then
                out[#out+1] = string.format(
out[#out + 1] = tostring(current_version)
end
out[#out+1] = string.format(
                     '===%s %s===',  
                     '===%s %s===',  
                     i18n.timeline.version,  
                     i18n.timeline.version,  
                     table.concat({v[1], v[2], 0}, '.')
                     table.concat({v[1], v[2], 0}, '.')
                 )  
                 )  
current_version = mw.html.create('ul')
                current_version = mw.html.create('ul')
end
            end
            current_version
current_version
                :tag('li')
:tag('li')
                    :wikitext(string.format(
:wikitext(string.format(
                         '%s - [[%s %s]]',
                         '%s - [[%s %s]]',  
                         h.date(row['versions.release_date']),
                         result[tpl_args.tables .. '.release_date'],  
                         i18n.timeline.version,
                         i18n.timeline.version,  
                         release_version,
                         release_version,  
                         row['versions.version'])
                         result[tpl_args.tables .. '.release_version'])
                     )
                     )
result_list = current_version:tag('ol')
            list = current_version:tag('ol')
end  
        end
 
         -- If it's not a version page then add another list with the
         -- List items
         -- results.
         if args.list_items then
if result[tpl_args.tables .. 'version'] == '' then  
             list
             -- If the result has an item class then the il-format can
                :tag('li')
            -- be used.
                    :wikitext(m_util.html.wikilink(row['items._pageName'], row['items.name']))
if  result[tpl_args.tables .. 'item_class'] ~= '' then
        end
result_list:tag('li'):wikitext(string.format('%s',
                    f_item_link{page=result[tpl_args.tables .. '._pageName']}
                    )
                )
else
result_list:tag('li'):wikitext(string.format('[[%s]]',  
                    result[tpl_args.tables .. '._pageName']
                    )
                )
               
end
end
          
          
         -- Save the last list
         -- Save the last list
if (i == #results) and (current_version ~= nil) then  
        if i == #results and current_version ~= nil then  
out[#out + 1] = tostring(current_version)
            out[#out + 1] = tostring(current_version)
end
        end
       
last_main_version = version_h2
        last_main_version = version_h2
last_minor_version = release_version
        last_minor_version = release_version
end
    end
 
-- Categories
     return table.concat(out, '\n') .. m_util.misc.add_category({i18n.categories.timelines})
     local cats = {
'Timelines',
    }
return table.concat(out, '\n') .. util.misc.add_category(cats)
end
end


-----
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
 
local p = {}
 
p.table_versions = m_cargo.declare_factory{data=tables.versions}
 
--
-- Template:Version
--
p.version = m_util.misc.invoker_factory(_version, {
    wrappers = cfg.wrappers.version,
})
 
--
-- Template:Version history list, Template:Timeline of items
--
p.timeline = m_util.misc.invoker_factory(_timeline)


return p
return p

Latest revision as of 22:01, 24 September 2024

Module documentation[view] [edit] [history] [purge]


Templates

-------------------------------------------------------------------------------
-- 
--                                Module:Version
-- 
-- This module implements Template:Version, Template:Version history list, and 
-- Template:Timeline of items
-------------------------------------------------------------------------------

require('Module:No globals')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_item_util -- Lazy load require('Module:Item util')

-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Version')

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Version/config/sandbox') or mw.loadData('Module:Version/config')

local i18n = cfg.i18n

-- ---------------------------------------------------------------------
-- Helper functions
-- ---------------------------------------------------------------------

local h = {}

function h.date(value, args)
    --[[
    Format dates in correct and useable form.
    
    Parameters
    ----------
    value : String, required
        Date
    args : Table
        Table with extra formatting args.
    
    ]]
    
    local args = args or {}
    
    -- List of allowed extra arguments:
    local arg_list = {
        format = {
            default = 'F j, Y H:i:s',
            cargo   = 'Y-m-d H:i:s',
            no_time = 'F j, Y',
        },
    }

    local lang = mw.getContentLanguage()
    local date_format = arg_list['format']['default']
    local timestamp = lang:formatDate(date_format, value)
    
    -- If the time is 00:00:00 then assume that the time isn't defined:
    if lang:formatDate('H:i:s', timestamp) == '00:00:00' then 
        date_format = arg_list['format']['no_time']
    end
    
    -- Add the extra arguments:
    for i,v in pairs(args) do
        if i == 'format' then
            date_format = arg_list[i][v]            
        end
    end
    
    -- Return the final timestamp format:
    local out
    if value ~= nil then
        out = lang:formatDate(date_format, timestamp)
    end
    
    return out
end

function h.validate_version(value)
    if value == nil then
        return value
    end
    return m_util.cast.version(value, {return_type='string'})
end

function h.show_date(args)
    return function(targs)
        local version = targs[args.key]
        local date = targs[string.format('%s_date', args.key)]
        if version and date then
            date = h.date(date) or ''
            if args.key == 'before' then
                return string.format(i18n.show_date.before, version, version, date)
            elseif args.key == 'after' then
                return string.format(i18n.show_date.after, version, version, date)
            end
        else
            return ''
        end
    end
end

-- ----------------------------------------------------------------------------
-- Cargo tables
-- ----------------------------------------------------------------------------

local tables = {}

tables.versions ={
    table = 'versions',
    fields = {
        patch = {
            field = 'version',
            type = 'String',
            func = h.validate_version,
        },
        patchdate = {
            field = 'release_date',
            type = 'Datetime',
            func = tostring,
        },
        major_part = {
            field = 'major_part',
            type = 'Integer',
        },
        minor_part = {
            field = 'minor_part',
            type = 'Integer',
        },
        patch_part = {
            field = 'patch_part',
            type = 'Integer',
        },
        revision_part = {
            field = 'revision_part',
            type = 'String',
        },
        before = {
            field = 'previous',
            type = 'String',
            func = h.validate_version,
            show = h.show_date{key='before'},
        },
        after = {
            field = 'after',
            type = 'String',
            func = h.validate_version,
            show = h.show_date{key='after'},
        },
    },
}

-- ----------------------------------------------------------------------------
-- Main functions
-- ----------------------------------------------------------------------------

local function _version(args)
    --[[
    Creates a version succession box and stores the data in a cargo table
    
    Example:
    p.version{
        before = '2.4.1',
        patch = '2.4.1b',
        patchdate = 'October 18, 2016',
        after = '2.4.2',
    }
    --]]

    -- Unpack args and validate
    for k, arg_def in pairs(tables.versions.fields) do
        if arg_def.func ~= nil then
            args[k] = arg_def.func(args[k])
        end
    end
    if not args.patch or not args.patchdate then
        error(i18n.version.required_args)
    end
    
    local version_parts = m_util.cast.version(args.patch, {return_type='table'})
    args.major_part = tonumber(version_parts[1])
    args.minor_part = tonumber(version_parts[2])
    args.patch_part = tonumber(version_parts[3])
    if version_parts[4] then
        args.revision_part = version_parts[4]
    end

    -- Validate 'before' and 'after' versions and query their release dates
    for _, key in ipairs({'before', 'after'}) do
        local version_number = args[key]
        if version_number then
            local results = m_cargo.query(
                {'versions'},
                {'versions.release_date=date'},
                {
                    where = string.format('versions.version="%s"', version_number)
                }
            )
            if #results == 1 then
                args[string.format('%s_date', key)] = results[1].date
            elseif #results > 1 then
                error(i18n.version.multiple_versions)
            end
        end
    end
    
    -- Store cargo data
    local data = {
        _table = tables.versions.table,
    }
    for k, v in pairs(tables.versions.fields) do
        if args[k] ~= nil then
            data[v.field] = args[k]
       end
    end
    m_cargo.store(data)

    mw.getCurrentFrame():expandTemplate{
        title = 'Template:Version/cargo/versions/attach'
    }

    -- Generate output
    local release_date = h.date(args.patchdate)
    local tbl = mw.html.create('table')
    tbl
        :addClass('wikitable successionbox')
        :tag('tr')
            :tag('th')
                :attr('colspan', 3)
                :wikitext(i18n.version.header)
                :done()
            :done()
        :tag('tr')
            :tag('td')
                :cssText('width: 30%')
                :wikitext(tables.versions.fields.before.show(args))
                :done()
            :tag('td')
                :cssText('width: 40%')
                :wikitext(string.format('<b>%s</b><br>%s', args.patch, release_date))
                :done()
            :tag('td')
                :cssText('width: 30%')
                :wikitext(tables.versions.fields.after.show(args))

    return tostring(tbl) .. m_util.misc.add_category({i18n.categories.versions})
end

local function _timeline(args)
    --[[ 
    Creates a version timeline and optionally lists items added to the game for each version
    
    Examples:
    p.timeline{
        where = 'versions.major_part = 0 AND versions.minor_part < 9',
    }
    
    p.timeline{
        list_items = true
        where = 'items.class_id = "DivinationCard"',
    }
    --]]

    local tables = {'versions'}
    local fields = {
        'versions.version',
        'versions.release_date',
    }
    local query = {
        orderBy = 'versions.major_part DESC, versions.minor_part DESC, versions.patch_part DESC, versions.revision_part DESC'
    }
    args.list_items = m_util.cast.boolean(args.list_items)
    if args.list_items then
        m_item_util = m_item_util or require('Module:Item util')
        table.insert(tables, 'items')
        fields = m_util.table.merge(fields, {'items._pageName', 'items.name'})
        query.join = 'versions.version=items.release_version'
        query.where = 'items.release_version IS NOT NULL'
        query.orderBy = query.orderBy .. ', items.name ASC'

        -- Namespace condition
        -- This is mainly to prevent items from user pages or other testing pages 
        -- from being returned in the query results.
        if args.namespaces ~= 'any' then
            local namespaces = m_util.cast.table(args.namespaces, {callback=m_util.cast.number})
            if #namespaces > 0 then
                namespaces = table.concat(namespaces, ',')
            else
                namespaces = m_item_util.get_item_namespaces{format = 'list'}
            end
            query.where = string.format('%s AND items._pageNamespace IN (%s)', query.where, namespaces)
        end
    end
    if args.where then
        -- m_util.table.merge rebuilds the table, which removes empty values
        -- TODO: Use a better function than m_util.table.merge
        query.where = table.concat(m_util.table.merge({query.where, args.where}), ' AND ')
    end
    local results = m_cargo.query(tables, fields, query)
    
    local out = {}
    local last_main_version 
    local last_minor_version
    local current_version
    local list
    
    -- Loop through all the results from the query
    for i, row in ipairs(results) do
        local release_version = row['versions.version']
        local v = m_util.cast.version(release_version)
        local version_h2 = table.concat({v[1], v[2]}, '.')
        if release_version ~= last_minor_version then
            if version_h2 ~= last_main_version then 
                if current_version ~= nil then
                    out[#out + 1] = tostring(current_version)
                end
                
                out[#out+1] = string.format(
                    '===%s %s===', 
                    i18n.timeline.version, 
                    table.concat({v[1], v[2], 0}, '.')
                ) 
                current_version = mw.html.create('ul')
            end
            current_version
                :tag('li')
                    :wikitext(string.format(
                        '%s - [[%s %s]]',
                        h.date(row['versions.release_date']),
                        i18n.timeline.version,
                        release_version,
                        row['versions.version'])
                    )
            list = current_version:tag('ol')
        end

        -- List items
        if args.list_items then
            list
                :tag('li')
                    :wikitext(m_util.html.wikilink(row['items._pageName'], row['items.name']))
        end
        
        -- Save the last list
        if i == #results and current_version ~= nil then 
            out[#out + 1] = tostring(current_version)
        end
        
        last_main_version = version_h2
        last_minor_version = release_version
    end

    return table.concat(out, '\n') .. m_util.misc.add_category({i18n.categories.timelines})
end

-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------

local p = {}

p.table_versions = m_cargo.declare_factory{data=tables.versions}

--
-- Template:Version
--
p.version = m_util.misc.invoker_factory(_version, {
    wrappers = cfg.wrappers.version,
})

--
-- Template:Version history list, Template:Timeline of items
--
p.timeline = m_util.misc.invoker_factory(_timeline)

return p