Module:Performances

From Angelina Jordan Wiki
Revision as of 13:00, 18 August 2024 by Most2dot0 (talk | contribs) (argtest funtion added)
The code for this module can be found below its documentation. You can edit the code, edit the documentation, or purge the cache.

This module is supposed to provide access to the list of performances Angelina did. At the heart of it is a very generic, configurable table-generator, that could also be used in different contexts.

Usage

Overview

{{#invoke: Performances | createTable       -- createTable is the generic table generator
  |page=Data:Performances.json              -- link to page with JSON data
  |supplements=Data:VideoMetaData.json;url  -- supplemental data sources
  |headers=Song,Date,Type,With,Video        -- List of titles used in the group section headers
  |keys=[[<song>]],{{d|<date>}},<type>,<with>,[<url> <song> - <event>] -- computed data items corresponding to those titles
  |sort=<date>                              -- computed sort keys for sorting outer table
  |sort1=<date>                             -- (optional): outer sortkey, same as inner if omitted
  |filters=date:2018,event:Kongsberg        -- filters to select what items are displayed
  |char_limit=7                             -- The amount of characters of the sort key considered for grouping the sections
  |char_limit1=4                            -- (optional): outer char limit, no outer grouping if ommited 
  |caption=Kongsberg 2018 by Month          -- Title displayed in Header of outer Table
  |group_sort=<date> ! <type> ! <pos>       -- how the group section sub-tables are sorted
  |id=1                                     -- Id needed if multiple tables are generated within a page
}}

Options

page: Provides page that contains JSON formated input data.

cargo_query: Alternative way to access data via Cargo query (see below for details)

supplements (optional): comma seperated list of supplemental JSON data source entries of "supplementalDataPage;keyName:charLimit;targetKeyName", with charLimit and targetKeyName being optional. Data from the supplemental source will be matched based on the keys (if the target's key name is ommitted, the same will be used). The charLimit option shortens the primary key value to this amount of characters. The target data can also be in a form, where the matching key data is used as keys itself instead of value of a "key" field.

headers: Comma seperated list of titles used in the group section headers.

keys: Comma seperated list of computed fields used for the colums in group sections defined by the corrosponding headers. Number of entries should match the number of headers.

Key names representing data are enclosed in "<",">" brackets. Each field can reference multiple keys. Other text will be still interpreted e.g. as templates. For the key's values, substitions can be defined by appending them with colons. A pair of strings will be interpretated as the search and replacement strings for a gsub() substition with lua pattern matching. The special keywords "toUpper", "toLower", and "toTitleCase" will perform the appropriate conversions, "limit:n" will limit the resresult to n characters. Several substitutions can be concatenated.

sort: Computed sort fields for sorting outer table, and for labeling of section names. Same syntax as for keys.

group (optional): Definition of how to label section names differently from 'sort' definition.

char_limit (optional): The amount of characters of the sort key considered for grouping the sections. This will also affect the displayed section name if 'group' is not provided.

filters: Comma seperated list of filters to select what items are displayed. A filter is described by "keyname:values". Alternatives within an filter value item can be described with "/". Negations can be described with "!( . )".

sort1 (optional): Outer sortkey. If ommited, the inner sort key will be used.

group1 (optional): Definition of how to label outer section names differently from 'sort1' definition.

char_limit1 (optional): The amount of characters of the sort key considered for the outer grouping. This will also affect the displayed section name, if 'group1' is ommited.

If ommited, no outer grouping will be performed.

id: A unique Id for each table is needed if multiple tables are generated within a page, to provide a basis for unique section ids.

caption (optional): Title displayed in the header of the outer table

group_sort: Computed field. that determines how the group section sub-tables are sorted. Same syntax as for keys.

Features

The generated table may have one or two levels of grouping, and up to three levels of sorting. It has the follwing structure, with the outer grouping being optional (click on the [n Items] buttons to reveal the inner collapsed tables):

Caption (provided as parameter)
1 (outer grouping level seperator)
A (inner grouping level header) [2 Items]
B (inner grouping level header) [1 Item]
2 (outer grouping level seperator)
C (inner grouping level header) [1 Item]

The inner tables are enclosed in

<div youtube-player-placeholder> ... </div> 

tags, which can optionally be used by Javascript code contained in a Gadget (MediaWiki:Gadget-embeddedYouTubePlayer.js) to attach an embedded YouTube player to each section, that is loaded with a playlist of any YouTube video link contained in that section. Clicking on those links will then also load and start the linked video in that player.

Each grouping separator/header will set an anchor id, so that the table rows can be jumped at with internal references, e.g. C.

There is also code available, that will expand the inner table of a jump target when jumped to.

Use of Cargo queries for input

This was recently added as an alternative source for input. It supports:

  • Multiple Cargo tables with configurable fields
  • Nested child tables (1–3+ levels)
  • List field splitting (# or ,)
  • Optional pretty-printed JSON
  • Flexible configuration in JSON or Lua-like syntax

The getJSON() function generates nested JSON structures from Cargo tables. It is meant to verify that queries produce the right structures before using them in the createTable() function.

All table and field configuration is contained in a single parameter: cargo_query.

{{#invoke:CargoQueryTest|getJSON
 |cargo_query=<JSON or Lua-like table>
 |pretty=true
}}
  • cargo_query – required; configuration of tables, fields, nesting, and root table.
  • pretty – optional; if "true", outputs indented JSON (requires mw.text.JSON_PRETTY).

cargo_query Parameters

Parameter Type Description
tables array of strings List of Cargo table names (CamelCase). Order does not determine root.
fields table Map of tableName → comma-separated list of field names to return. Fields can be lowercase.
where table Map of tableName → Cargo WHERE clause for filtering rows. Empty string for no filter.
nest array of tables Each table describes a parent-child nesting rule: { parent = "ParentTable", child = "ChildTable", parentKey = "ParentField", childKey = "ChildField", as = "ChildLabel" }
listFields array of strings Field names that should be returned as arrays (split on # or ,).
root string The Cargo table to use as the top-level JSON object.

Field Splitting Rules

  • # delimiter → splits Cargo lists into an array, preserves empty entries.
 Example: "Concert##US Concert" → ["Concert","","US Concert"]  
  • ', delimiter → splits only if there are no spaces around the comma.

Example 1 — Two-level nesting (Performances → Videos)

{{#invoke:CargoQueryTest|getJSON
 |cargo_query={
   "tables": ["PerformancesDevel","VideosDevel"],
   "fields": {
     "PerformancesDevel": "song,event,context,date,type,pos,partners,comment,perfID",
     "VideosDevel": "perfID,url,duration,quality"
   },
   "where": {
     "PerformancesDevel": "",
     "VideosDevel": ""
   },
   "nest": [
     { "parent": "PerformancesDevel", "child": "VideosDevel", "parentKey": "perfID", "childKey": "perfID", "as": "videos" }
   ],
   "listFields": ["context","partners"],
   "root": "PerformancesDevel"
 }
 |pretty=true
}}

Lua-like table style (sanitized)

{{#invoke:CargoQueryTest|getJSON
 |cargo_query={
   tables = ['PerformancesDevel','VideosDevel'],
   fields = {
     PerformancesDevel = 'song,event,context,date,type,pos,partners,comment,perfID',
     VideosDevel = 'perfID,url,duration,quality'
   },
   nest = {
     { parent = 'PerformancesDevel', child = 'VideosDevel', parentKey = 'perfID', childKey = 'perfID', as = 'videos' }
   },
   listFields = ['context','partners'],
   root = 'PerformancesDevel'
 }
 |pretty=true
}}

Example 2 — Three-level nesting (Songs → Performances → Videos)

{{#invoke:CargoQueryTest|getJSON
 |cargo_query={
   tables = ['SongsDevel','PerformancesDevel','VideosDevel'],
   fields = {
     SongsDevel = 'songID,title,artist',
     PerformancesDevel = 'song,event,context,date,type,pos,partners,comment,perfID',
     VideosDevel = 'perfID,url,duration,quality'
   },
   nest = {
     { parent = 'SongsDevel', child = 'PerformancesDevel', parentKey = 'songID', childKey = 'song', as = 'performances' },
     { parent = 'PerformancesDevel', child = 'VideosDevel', parentKey = 'perfID', childKey = 'perfID', as = 'videos' }
   },
   listFields = ['context','partners'],
   root = 'SongsDevel'
 }
 |pretty=true
}}

Tips

  1. Always specify root in cargo_query to avoid ambiguity.
  2. Use listFields for any Cargo field that should become a Lua/JSON array.
  3. pretty=true is optional but recommended for debugging and readability.
  4. Lua-table style is allowed inline — single quotes ' are fine; the module sanitizes them into valid JSON.
  5. Multi-level nesting must specify both parent and child explicitly in each rule.
  6. Use the AJW:Cargo_query_test page to develop your query and check if it delivers the expected JSON equivalent structure.

ToDo

  • Make sorting more flexible, e.g. by applying regex substitutions to data values before being sorted.
  • Have an option, to not start collapsed (usefull for small tables)
  • Implement access to sublists below keys.
  • provide default values for empty fields of computed fields, e.g. like <keyName=defaultValue>
  • Provide a means to print additional data behind the sort field that does not effect the id-anchor

Examples

Multiple Filters, combined as AND

{{#invoke: Performances | createTable
  |page=Data:Performances.json
  |headers=Song,Date,Type,With,Video
  |keys=[[<song>]],{{d|<date>}},<type>,<with>,[<url> <song> - <event>]
  |sort=<date>
  |filters=date:2018,event:Kongsberg
  |char_limit=7
  |caption=Kongsberg 2018 by Month
  |group_sort=<date> ! <type> ! <pos>
  |id=1
}}

Error: headers, keys, and sort arguments are required.


Single Event with Supplemental Video Metadata

{{#invoke: Performances | createTable
  |page=Data:Performances.json
  |supplements=Data:VideoMetaData.json;url:43
  |headers=Song,Date,Type,Video
  |keys=[[<song>]],{{d|<date>}},<type>,[<url> <url-title>; <url-User>]
  |sort=<event>
  |group_sort=<date> ! <type> ! <pos> ! <song>
  |filters=event:Bjerke
  |id=2
}}

Error: headers, keys, and sort arguments are required.

Alternatives in Filters, combinded as OR

{{#invoke: Performances | createTable
  |page=Data:Performances.json
  |headers=Song,Date,Type,Comment
  |keys=[[#<song>|<song>]],{{d|<date>}}: <pos>,<type> <duration> [<url> play],<comment>; <with>
  |sort=[[<event>]]
  |caption=Repetitive Live Events
  |group_sort=<date> ! <type> ! <pos> ! <song>
  |filters=event:Allsang på Grensen/TV 2's Artist Gala
  |id=3
}}

Error: headers, keys, and sort arguments are required.

Demonstrating Different Outer Sorting/Grouping and Negation in Filters

In the following example, the highest level (sort1) is sorted by year, then the middle one (sort) by date, as well as the inner table one, which also features secondary sorting keys (type, pos, song). The seperator "!" that was chosen for these has no special meaning, but it is the lowest character encoding value above a space, which should ensure that the correct sorting is applied.

"duration:!(fragment)" is used for filtering for those videos, that are not fragments.

Also, wie use substitutions to the computed "url" field

{{#invoke: Performances | createTable
  |page=Data:Performances.json
  |headers=Song,Date,Type,Comment,Video
  |keys=[[#<song>|<song>]],{{d|<date>}}; <pos>,<type> <duration>, <comment>; <with>,[<url> <url:.*www.: :.com.*: >]
  |sort=[[<event>]]
  |sort1=<date>
  |char_limit1=4
  |filters=type:live,duration:!(fragment)
  |caption=Live Events by Year, excluding fragments
  |group_sort=<date> ! <type> ! <pos> ! <song>
  |id=4
}}
Jump to… 2013 – 2014 – 2015 – 2016 – 2017 – 2018 – 2019 – 2020 – 2021 – 2022 – 2023 – 2024

Error: headers, keys, and sort arguments are required.

Jump to… 2013 – 2014 – 2015 – 2016 – 2017 – 2018 – 2019 – 2020 – 2021 – 2022 – 2023 – 2024

Demonstrating Same Type Sorting, with Different Grouping Levels, and Supplemental Data

Here, the different char_limits on the sorting of song lead to two different grouping levels, by 1st character and by unique title.

{{AtoZ}}
{{#invoke: Performances | createTable
  |page=Data:Performances.json
  |supplements=Data:Songs.json;song;title,Data:VideoMetaData.json;url:43  |headers=Event,Date,Type,Video,Pos,With,Comment
  |keys=[[#<event>|<event>]],{{d|<date>}},<type> <duration>,[<url> play <song-type>],<pos>,<with>,<comment>; <url-channelName>
  |sort=[[<song>]]
  |char_limit1=1
  |caption=Live Songs, excluding fragments
  |group_sort=<date> ! <type> ! <pos>
  |filters=duration:!(fragment),type:live
  |id=5
}}
{{AtoZ}}

A · B · C · D · E · F · G · H · I · J · K · L · M · N · O · P · Q · R · S · T · U · V · W · X · Y · Z

Error: headers, keys, and sort arguments are required.

A · B · C · D · E · F · G · H · I · J · K · L · M · N · O · P · Q · R · S · T · U · V · W · X · Y · Z

Code

[Edit module code]


local p = {}

-- Load required libraries
local mw_text = mw.text
local mw_title = require('mw.title')

-- Function to fetch JSON data from a wiki page
function p.fetchJSONFromPage(pageName)
    local title = mw_title.new(pageName)
    if not title then
        return nil, 'Invalid page name'
    end
    local content = title:getContent()
    if not content then
        return nil, 'Page not found or empty'
    end
    return mw_text.jsonDecode(content)
end

-- Function to compute a dynamic field value based on a formula
function p.computeField(item, formula)
    return (formula:gsub('(%b{})', function(placeholder)
        local key = placeholder:sub(2, -2)
        return tostring(item[key] or '')
    end))
end

-- Function to filter data based on pattern matching
function p.filterData(data, filters)
    local filteredData = {}
    
    for _, item in ipairs(data) do
        local match = true
        
        for key, pattern in pairs(filters) do
            local luaPattern = pattern:gsub('%*', '.*')
            if not tostring(item[key]):match(luaPattern) then
                match = false
                break
            end
        end
        
        if match then
            table.insert(filteredData, item)
        end
    end
    
    return filteredData
end

-- Sorting function
function p.sortData(data, sortKeyFormula)
    table.sort(data, function(a, b)
        local aKey = p.computeField(a, sortKeyFormula)
        local bKey = p.computeField(b, sortKeyFormula)
        return tostring(aKey) < tostring(bKey)
    end)
    return data
end

-- Function to group data by a key with an optional character limit
function p.groupData(data, groupKeyFormula, charLimit)
    local groupedData = {}
    local currentGroup = nil

    for _, item in ipairs(data) do
        local groupValue = p.computeField(item, groupKeyFormula)
        if charLimit and tonumber(charLimit) then
            groupValue = mw.ustring.sub(groupValue, 1, tonumber(charLimit))
        end
        
        if groupValue ~= currentGroup then
            currentGroup = groupValue
            groupedData[#groupedData + 1] = {header = groupValue, items = {}}
        end
        table.insert(groupedData[#groupedData].items, item)
    end

    return groupedData
end

-- Function to generate table headers
function p.renderHeaders(headers)
    local headerRow = '! ' .. table.concat(headers, ' !! ') .. '\n'
    return headerRow
end

-- Function to generate table rows
function p.renderRows(items, computedKeys)
    local rows = ''
    for _, item in ipairs(items) do
        rows = rows .. '|-\n'
        local row = {}
        for _, keyFormula in ipairs(computedKeys) do
            row[#row + 1] = p.computeField(item, keyFormula) or ''
        end
        rows = rows .. '| ' .. table.concat(row, ' || ') .. '\n'
    end
    return rows
end

-- Function to render the table structure with collapsible middle table
function p.renderTable(data, headers, computedKeys, sortKeyFormula, sortHeaderName, charLimit, groupSortKeyFormula)
    local sortedData = p.sortData(data, sortKeyFormula)
    local groupedData = p.groupData(sortedData, sortKeyFormula, charLimit)
    
    local outerTable = '{| class="wikitable" style="width: 100%; margin: 0;"\n'
    outerTable = outerTable .. '|+ ' .. sortHeaderName .. '\n'
    
    for idx, group in ipairs(groupedData) do
        if groupSortKeyFormula then
            p.sortData(group.items, groupSortKeyFormula)
        end
        
        outerTable = outerTable .. '|-\n| \n'
        outerTable = outerTable .. '{| class="wikitable mw-collapsible mw-collapsed" style="width: 100%; margin: 0;"\n'
        outerTable = outerTable .. '|+ ' .. group.header .. '\n'
        outerTable = outerTable .. '|-\n| <div class="youtube-player-placeholder">\n'
        outerTable = outerTable .. '{| class="wikitable sortable" style="width: 100%; margin: 0;"\n'
        outerTable = outerTable .. '|-\n' .. p.renderHeaders(headers)
        outerTable = outerTable .. p.renderRows(group.items, computedKeys)
        outerTable = outerTable .. '|}\n</div>\n'
        outerTable = outerTable .. '|}\n'
    end

    outerTable = outerTable .. '|}\n'
    return outerTable
end

-- Main function that accepts JSON from a string or a wiki page
function p.createTable(frame)
    local args = frame:getParent().args
    local jsonStr = args['data']
    local pageName = args['page']

    if not args['headers'] or not args['keys'] or not args['sort'] then
        return 'Error: headers, keys, and sort arguments are required.'
    end

    local headers = mw_text.split(args['headers'], ',')
    local computedKeys = mw_text.split(args['keys'], ',')
    local sortKeyFormula = args['sort']
    local charLimit = args['char_limit']
    local groupSortKeyFormula = args['group_sort']

    local filters = {}
    local filterString = args['filters']
    if filterString then
        for key, pattern in filterString:gmatch('([^:]+):([^,]+),?') do
            filters[key] = pattern
        end
    end

    local data
    local err

    if pageName then
        data, err = p.fetchJSONFromPage(pageName)
        if not data then
            return 'Error fetching JSON from page: ' .. err
        end
    else
        data = mw_text.jsonDecode(jsonStr)
        if not data then
            return 'Error decoding JSON.'
        end
    end

    data = p.filterData(data, filters)

    local sortHeaderName = nil
    for i, key in ipairs(computedKeys) do
        if key == sortKeyFormula then
            sortHeaderName = headers[i]
            break
        end
    end

    if not sortHeaderName then
        return 'Error: Sort key does not match any provided keys.'
    end

    return p.renderTable(data, headers, computedKeys, sortKeyFormula, sortHeaderName, charLimit, groupSortKeyFormula)
end



function p.test() 
	-- Assuming the module is named 'YourModuleName', and it's already required or available
	-- local YourModule = require('Module:YourModuleName')
	
	-- Define the JSON data
	local jsonData = '[{"name":"Alice","age":30,"city":"New York"},{"name":"Bob","age":25,"city":"San Francisco"},{"name":"Charlie","age":30,"city":"Los Angeles"}]'
	local jsonPage = 'Data:Performances.json'
	-- Define the headers and keys
	local headers = "Song,Event,Date,Type,Pos,With,Comment,Video"
	local keys = "[[{song}]],[[{event}]],{date},{type},{pos},{with},{comment},[{url} {song} - {event}]"
	local sortKey = "{date}"
	local char_limit = "4"
	local group_sort = "{date} - {type} - {pos}"
	local filters = "date:2018,event:Kongsberg"
	
	-- Manually create a frame-like table structure to pass arguments
	local fakeFrame = {
	    args = {
	        --data = jsonData,    -- Pass the JSON string
	        page = jsonPage,    -- Pase the JSON page
	        headers = headers,  -- Pass the headers as a comma-separated string
	        keys = keys,        -- Pass the keys as a comma-separated string
	        sort = sortKey,      -- Specify the key to sort by
	        char_limit = char_limit,
	        group_sort = group_sort,
	        filters = filters
	    },
	    getParent = function(self) return self end
	}

	-- Call the function directly with the fake frame
	local result = p.createTable(fakeFrame)
	
	-- Output or further process the result
	mw.logObject(result)
	return result
end

-- Function to test arguments passed from the wiki page
function p.argtest(frame)
    local args = frame:getParent().args

    -- Collecting output
    local output = {}
    output[#output + 1] = "Number of arguments: " .. tostring(#args)

    -- Possible arguments to check
    local possibleArgs = {"data", "page", "headers", "keys", "sort", "filters", "char_limit", "group_sort"}

    for _, arg in ipairs(possibleArgs) do
        if args[arg] then
            output[#output + 1] = arg .. ": provided"
        else
            output[#output + 1] = arg .. ": not provided"
        end
    end

    -- Output the results in <pre> tags
    return "<pre>" .. table.concat(output, "\n") .. "</pre>"
end

return p