From 839a15c1a8768de6dcce0f4895413da8c116c8a9 Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Sun, 1 Mar 2026 18:39:54 +0100 Subject: [PATCH 01/17] Add initial draft with storage+fetch --- lua/wikis/counterstrike/VRSStandings.lua | 215 +++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 lua/wikis/counterstrike/VRSStandings.lua diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua new file mode 100644 index 00000000000..65fa13183ce --- /dev/null +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -0,0 +1,215 @@ +--- +-- @Liquipedia +-- page=Module:Widget/VRSStandings.lua +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local Class = Lua.import('Module:Class') +local Date = Lua.import('Module:Date/Ext') +local FnUtil = Lua.import('Module:FnUtil') +local Json = Lua.import('Module:Json') +local Logic = Lua.import('Module:Logic') +local Lpdb = Lua.import('Module:Lpdb') +local MathUtil = Lua.import('Module:MathUtil') +local Operator = Lua.import('Module:Operator') +local Opponent = Lua.import('Module:Opponent') +local PlayerDisplay = Lua.import('Module:Player/Display') +local OpponentDisplay = Lua.import('Module:OpponentDisplay') +local Table = Lua.import('Module:Table') + +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local Widget = Lua.import('Module:Widget') +local WidgetUtil = Lua.import('Module:Widget/Util') +local HtmlWidgets = Lua.import('Module:Widget/Html/All') + +local Condition = Lua.import('Module:Condition') +local BooleanOperator = Condition.BooleanOperator +local Comparator = Condition.Comparator + + +local DATAPOINT_TYPE = 'vrs_ranking' + +---@class VRSStandings: Widget +---@operator call(table): VRSStandings +---@field props table +local VRSStandings = Class.new(Widget) +VRSStandings.defaultProps = { + title = 'VRS Standings', +} + +---@return Widget? +function VRSStandings:render() + local standings, settings = self:_parse() + + local headerRow = TableWidgets.TableHeader{children = { + TableWidgets.Row{children = WidgetUtil.collect( + TableWidgets.CellHeader{children = {'Rank'}}, + TableWidgets.CellHeader{children = {'Points'}}, + TableWidgets.CellHeader{children = {HtmlWidgets.Span{children = 'Team', css = {['text-align'] = 'center', display = 'block'}}}}, + TableWidgets.CellHeader{children = {HtmlWidgets.Span{children = 'Roster', css = {['text-align'] = 'center', display = 'block'}}}} + )} + }} + + local title = HtmlWidgets.Div { + children = { + HtmlWidgets.Div { + children = { + HtmlWidgets.B { children = 'Unofficial VRS' }, + HtmlWidgets.Span { children = 'Last updated: ' .. settings.updated } + }, + classes = { 'ranking-table__top-row-text' } + }, + HtmlWidgets.Div { + children = { + HtmlWidgets.Span { children = 'Data by Liquipedia' }, + }, + classes = { 'ranking-table__top-row-logo-container' } + } + }, + classes = { 'ranking-table__top-row' }, + } + + return TableWidgets.Table{ + title = title, + sortable = false, + columns = WidgetUtil.collect( + { + align = 'center', + sortType = 'number' + }, + { + align = 'right', + sortType = 'number', + }, + { + align = 'left' + }, + { + align = 'left' + } + ), + children = { + headerRow, + TableWidgets.TableBody{children = Array.map(standings, VRSStandings._row)} + }, + } +end + +---@private +---@return {place: number, points: number, opponent: standardOpponent}[] +---@return {title: string, updated: string, shouldStore: boolean, shouldFetch: boolean} +function VRSStandings:_parse() + local props = self.props + local settings = { + title = props.title, + updated = Date.toYmdInUtc(Date.parseIsoDate(props.updated) or os.date('%F')), + shouldFetch = Logic.readBool(props.shouldFetch), + fetchLimit = tonumber(props.fetchLimit) + } + + ---@type {points: number, opponent: standardOpponent}[] + local standings = {} + + if settings.shouldFetch then + standings = self._fetch(settings.updated, settings.fetchLimit) + else + Table.iter.forEachPair(self.props, function(key, value) + if not string.match(key, '^%d+$') then + return + end + + local data = Json.parse(value) + local opponent = Opponent.readOpponentArgs(Table.merge(data, { + type = Opponent.team, + })) + + -- Remove template from data to not confuse it with first player + data[1] = nil + opponent.players = Array.map(Array.range(1, 5), FnUtil.curry(Opponent.readPlayerArgs, data)) + + table.insert(standings, { + place = tonumber(key), + points = tonumber(data.points), + opponent = opponent + }) + end) + + self._store(settings.updated, standings) + end + + Array.sortInPlaceBy(standings, Operator.property('place')) + + return standings, settings +end + +---@private +---@param standing {place: number, points: number, opponent: standardOpponent} +---@return Widget +function VRSStandings._row(standing) + return TableWidgets.Row{children = WidgetUtil.collect( + TableWidgets.Cell{ + children = standing.place, + }, + TableWidgets.Cell{ + children = MathUtil.formatRounded{value = standing.points, precision = 1} + }, + TableWidgets.Cell{ + children = OpponentDisplay.InlineTeamContainer{ + template = standing.opponent.template + } + }, + TableWidgets.Cell{ + children = Array.map(standing.opponent.players, function(player) + return HtmlWidgets.Span{ + css = { + display = "inline-block", + width = "160px" + }, + children = PlayerDisplay.InlinePlayer({player = player}) + } + end), + } + )} +end + +---@private +---@param updated string +---@param standings {place: number, points: number, opponent: standardOpponent}[] +function VRSStandings._store(updated, standings) + if Lpdb.isStorageDisabled() then + return + end + local dataPoint = Lpdb.DataPoint:new{ + objectname = 'vrs_' .. updated, + type = DATAPOINT_TYPE, + name = 'Inofficial VRS (' .. updated .. ')', + date = updated, + extradata = standings + } + dataPoint:save() +end + +---@private +---@param updated string +---@param fetchLimit integer +---@return {place: number, points: number, opponent: standardOpponent}[] +function VRSStandings._fetch(updated, fetchLimit) + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = Condition.Tree(BooleanOperator.all):add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE), + Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), + Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 2), -- TODO: Remove before release + }:toString(), + query = 'extradata', + limit = 1, + }) + + assert(data[1], 'No VRS data found') + return Array.sub(data[1].extradata, 1, fetchLimit) +end + +return VRSStandings From b14ef39867d51022350e664e3dbeb8b6877b2b39 Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Sun, 1 Mar 2026 19:21:22 +0100 Subject: [PATCH 02/17] Remove header css --- lua/wikis/counterstrike/VRSStandings.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index 65fa13183ce..dd46e3345b5 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -47,10 +47,10 @@ function VRSStandings:render() local headerRow = TableWidgets.TableHeader{children = { TableWidgets.Row{children = WidgetUtil.collect( - TableWidgets.CellHeader{children = {'Rank'}}, - TableWidgets.CellHeader{children = {'Points'}}, - TableWidgets.CellHeader{children = {HtmlWidgets.Span{children = 'Team', css = {['text-align'] = 'center', display = 'block'}}}}, - TableWidgets.CellHeader{children = {HtmlWidgets.Span{children = 'Roster', css = {['text-align'] = 'center', display = 'block'}}}} + TableWidgets.CellHeader{children = 'Rank'}, + TableWidgets.CellHeader{children = 'Points'}, + TableWidgets.CellHeader{children = 'Team'}, + TableWidgets.CellHeader{children = 'Roster'} )} }} From e10fa9fb1de9a0299914a9409130cac1d8c72240 Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Sun, 1 Mar 2026 19:36:30 +0100 Subject: [PATCH 03/17] Rank alignment --- lua/wikis/counterstrike/VRSStandings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index dd46e3345b5..5087722cab7 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -78,7 +78,7 @@ function VRSStandings:render() sortable = false, columns = WidgetUtil.collect( { - align = 'center', + align = 'right', sortType = 'number' }, { From ebd81338076149ea44534066d6fd3afad6dbc066 Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Sun, 1 Mar 2026 19:36:50 +0100 Subject: [PATCH 04/17] tabs not spaces --- lua/wikis/counterstrike/VRSStandings.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index 5087722cab7..664e62c3352 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -79,18 +79,18 @@ function VRSStandings:render() columns = WidgetUtil.collect( { align = 'right', - sortType = 'number' + sortType = 'number' }, { align = 'right', sortType = 'number', }, - { - align = 'left' - }, - { - align = 'left' - } + { + align = 'left' + }, + { + align = 'left' + } ), children = { headerRow, From cbb3be64d256fe1be23523aba6c954a2adc28d2d Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Wed, 11 Mar 2026 19:02:45 +0100 Subject: [PATCH 05/17] Apply suggestion from review Co-authored-by: hjpalpha <75081997+hjpalpha@users.noreply.github.com> --- lua/wikis/counterstrike/VRSStandings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index 664e62c3352..a6733a18b22 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -106,7 +106,7 @@ function VRSStandings:_parse() local props = self.props local settings = { title = props.title, - updated = Date.toYmdInUtc(Date.parseIsoDate(props.updated) or os.date('%F')), + updated = Date.toYmdInUtc(props.updated or DateExt.getCurrentTimestamp()), shouldFetch = Logic.readBool(props.shouldFetch), fetchLimit = tonumber(props.fetchLimit) } From 1af0f2de7729585bfa0bc105794f6c855973a342 Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Wed, 11 Mar 2026 19:04:39 +0100 Subject: [PATCH 06/17] Update VRSStandings.lua --- lua/wikis/counterstrike/VRSStandings.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index a6733a18b22..f119a86dd37 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -9,7 +9,7 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') -local Date = Lua.import('Module:Date/Ext') +local DateExt = Lua.import('Module:Date/Ext') local FnUtil = Lua.import('Module:FnUtil') local Json = Lua.import('Module:Json') local Logic = Lua.import('Module:Logic') @@ -106,7 +106,7 @@ function VRSStandings:_parse() local props = self.props local settings = { title = props.title, - updated = Date.toYmdInUtc(props.updated or DateExt.getCurrentTimestamp()), + updated = DateExt.toYmdInUtc(props.updated or DateExt.getCurrentTimestamp()), shouldFetch = Logic.readBool(props.shouldFetch), fetchLimit = tonumber(props.fetchLimit) } From 6a510cf77a1ef40814546083df391ae603be77c8 Mon Sep 17 00:00:00 2001 From: SyntacticSalt Date: Thu, 12 Mar 2026 09:08:33 +0100 Subject: [PATCH 07/17] Remove "Remove before release" Co-authored-by: hjpalpha <75081997+hjpalpha@users.noreply.github.com> --- lua/wikis/counterstrike/VRSStandings.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index f119a86dd37..6dfcd65f840 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -202,7 +202,6 @@ function VRSStandings._fetch(updated, fetchLimit) conditions = Condition.Tree(BooleanOperator.all):add{ Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE), Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), - Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 2), -- TODO: Remove before release }:toString(), query = 'extradata', limit = 1, From e8fe01438993215a5964260d3fcb0a04bddfd7fe Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 20 Mar 2026 09:31:29 +0000 Subject: [PATCH 08/17] updating vrsstandings functionality (#7279) adding live / main as well as filter functionality --- .../counterstrike/MainPageLayout/data.lua | 19 +- lua/wikis/counterstrike/VRSStandings.lua | 379 ++++++++++++++---- 2 files changed, 322 insertions(+), 76 deletions(-) diff --git a/lua/wikis/counterstrike/MainPageLayout/data.lua b/lua/wikis/counterstrike/MainPageLayout/data.lua index 7fdf0d9e218..525fd1f35f1 100644 --- a/lua/wikis/counterstrike/MainPageLayout/data.lua +++ b/lua/wikis/counterstrike/MainPageLayout/data.lua @@ -18,6 +18,7 @@ local FilterButtonsWidget = Lua.import('Module:Widget/FilterButtons') local ThisDayWidgets = Lua.import('Module:Widget/MainPage/ThisDay') local TransfersList = Lua.import('Module:Widget/MainPage/TransfersList') local WantToHelp = Lua.import('Module:Widget/MainPage/WantToHelp') +local VRSStandings = Lua.import('Module:Widget/VRSStandings') local CONTENT = { @@ -74,6 +75,16 @@ local CONTENT = { padding = true, boxid = MainPageLayoutUtil.BoxId.TOURNAMENTS_TICKER, }, + vrsStandings = { + heading = 'Valve Regional Standings', + body = VRSStandings{ + shouldFetch = 1, + fetchLimit = 5, + mainpage = 1, + }, + padding = false, + boxid = 1521, + } } return { @@ -151,13 +162,17 @@ return { mobileOrder = 1, content = CONTENT.specialEvents, }, + { + mobileOrder = 3, + content = CONTENT.vrsStandings, + }, { mobileOrder = 4, - content = CONTENT.thisDay, + content = CONTENT.transfers, }, { mobileOrder = 5, - content = CONTENT.transfers, + content = CONTENT.thisDay, }, { mobileOrder = 7, diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index 6dfcd65f840..c051d73ceb5 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -30,8 +30,13 @@ local Condition = Lua.import('Module:Condition') local BooleanOperator = Condition.BooleanOperator local Comparator = Condition.Comparator +local Link = Lua.import('Module:Widget/Basic/Link') +local Icon = Lua.import('Module:Icon') -local DATAPOINT_TYPE = 'vrs_ranking' + +local DATAPOINT_TYPE_LIVE = 'vrs_ranking_live' +local DATAPOINT_TYPE_MAIN = 'vrs_ranking' +local FOOTER_LINK = 'Valve_Regional_Standings' ---@class VRSStandings: Widget ---@operator call(table): VRSStandings @@ -39,26 +44,64 @@ local DATAPOINT_TYPE = 'vrs_ranking' local VRSStandings = Class.new(Widget) VRSStandings.defaultProps = { title = 'VRS Standings', + filterRegion = nil, + filterSubregion = nil, + filterCountry = nil, + mainpage = nil, + rankingType = 'live', } ---@return Widget? function VRSStandings:render() local standings, settings = self:_parse() - local headerRow = TableWidgets.TableHeader{children = { - TableWidgets.Row{children = WidgetUtil.collect( + local headerCells + if settings.filterType ~= 'none' then + headerCells = WidgetUtil.collect( + TableWidgets.CellHeader{children = 'Rank'}, + TableWidgets.CellHeader{children = 'Global Rank'}, + TableWidgets.CellHeader{children = 'Points'}, + TableWidgets.CellHeader{children = 'Team'} + ) + else + headerCells = WidgetUtil.collect( TableWidgets.CellHeader{children = 'Rank'}, TableWidgets.CellHeader{children = 'Points'}, TableWidgets.CellHeader{children = 'Team'}, - TableWidgets.CellHeader{children = 'Roster'} - )} - }} + TableWidgets.CellHeader{children = 'Region'} + ) + end + + if not settings.mainpage then + table.insert(headerCells, TableWidgets.CellHeader{children = 'Roster'}) + end + + local headerRow = TableWidgets.TableHeader{ + children = { + TableWidgets.Row{children = headerCells} + } + } + local regionMap = { + AS = 'Asia', + AM = 'Americas', + EU = 'Europe' + } + + local titleName = 'Global' + + if settings.filterType == 'region' then + titleName = regionMap[settings.filterRegion] or settings.filterRegion + elseif settings.filterType == 'subregion' then + titleName = 'Subregion' + elseif settings.filterType == 'country' then + titleName = settings.filterCountryDisplay + end local title = HtmlWidgets.Div { children = { HtmlWidgets.Div { children = { - HtmlWidgets.B { children = 'Unofficial VRS' }, + HtmlWidgets.B { children = 'Unofficial ' .. titleName .. ' VRS' }, HtmlWidgets.Span { children = 'Last updated: ' .. settings.updated } }, classes = { 'ranking-table__top-row-text' } @@ -73,49 +116,134 @@ function VRSStandings:render() classes = { 'ranking-table__top-row' }, } - return TableWidgets.Table{ - title = title, - sortable = false, + local columns + if settings.filterType ~= 'none' then columns = WidgetUtil.collect( - { - align = 'right', - sortType = 'number' - }, - { - align = 'right', - sortType = 'number', - }, - { - align = 'left' - }, - { - align = 'left' + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'} + ) + else + columns = WidgetUtil.collect( + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'}, + {align = 'center'} + ) + end + + if settings.mainpage then + for _, col in ipairs(columns) do + col.width = (100 / #columns) .. '%' + end + end + + if not settings.mainpage then + table.insert(columns, {align = 'left'}) + end + + local footer = Link { + link = FOOTER_LINK, + linktype = 'internal', + children = { + HtmlWidgets.Div { + children = { 'See Rankings Page', Icon.makeIcon { iconName = 'goto' } }, + classes = { 'ranking-table__footer-button' }, } - ), + }, + } + + if #standings == 0 then + return HtmlWidgets.Div{ + children = { + HtmlWidgets.B{ children = 'No teams found for the selected filter.' } + }, + css = { padding = '12px' } + } + end + + local tableWidget = TableWidgets.Table{ + title = title, + sortable = false, + columns = columns, + footer = settings.mainpage and footer or nil, + css = settings.mainpage and { width = '100%' } or nil, children = { headerRow, - TableWidgets.TableBody{children = Array.map(standings, VRSStandings._row)} + TableWidgets.TableBody{ + children = Array.map(standings, function(entry) + return VRSStandings._row(entry, settings.mainpage) + end) + } }, } + + if settings.mainpage then + return HtmlWidgets.Div{ + css = { width = '100%' }, + children = { tableWidget } + } + else + return tableWidget + end end ---@private ----@return {place: number, points: number, opponent: standardOpponent}[] ----@return {title: string, updated: string, shouldStore: boolean, shouldFetch: boolean} function VRSStandings:_parse() local props = self.props + local rankingType = (props.rankingType == 'main') and 'main' or 'live' + local datapointType = (rankingType == 'main') and DATAPOINT_TYPE_MAIN or DATAPOINT_TYPE_LIVE + local settings = { title = props.title, - updated = DateExt.toYmdInUtc(props.updated or DateExt.getCurrentTimestamp()), shouldFetch = Logic.readBool(props.shouldFetch), - fetchLimit = tonumber(props.fetchLimit) + fetchLimit = tonumber(props.fetchLimit), + filterRegion = props.filterRegion, + filterSubregion = props.filterSubregion, + filterCountry = props.filterCountry, + mainpage = Logic.readBool(props.mainpage), + rankingType = rankingType, + datapointType = datapointType, } + if props.updated == 'latest' or not props.updated then + settings.updated = VRSStandings._fetchLatestDate(datapointType) + else + settings.updated = DateExt.toYmdInUtc(props.updated) + end + + -- Only one filter can be applied at once + settings.filterType = 'none' + + if settings.filterRegion then + settings.filterType = 'region' + elseif settings.filterSubregion then + settings.filterType = 'subregion' + elseif settings.filterCountry then + settings.filterType = 'country' + end + + settings.filterCountries = nil + settings.filterCountryDisplay = 'Filtered' + + if settings.filterCountry then + local rawList = mw.text.split(settings.filterCountry, ',') + local countrySet = {} + + for _, raw in ipairs(rawList) do + countrySet[mw.text.trim(raw)] = true + end + + settings.filterCountries = countrySet + settings.filterCountryDisplay = #rawList > 1 and 'Filtered' or mw.text.trim(rawList[1]) + end + ---@type {points: number, opponent: standardOpponent}[] local standings = {} if settings.shouldFetch then - standings = self._fetch(settings.updated, settings.fetchLimit) + standings = VRSStandings._fetch(settings.updated, settings.datapointType) else Table.iter.forEachPair(self.props, function(key, value) if not string.match(key, '^%d+$') then @@ -123,92 +251,195 @@ function VRSStandings:_parse() end local data = Json.parse(value) + local opponent = Opponent.readOpponentArgs(Table.merge(data, { type = Opponent.team, })) - -- Remove template from data to not confuse it with first player data[1] = nil - opponent.players = Array.map(Array.range(1, 5), FnUtil.curry(Opponent.readPlayerArgs, data)) + opponent.players = Array.map(Array.range(1,5), FnUtil.curry(Opponent.readPlayerArgs, data)) + + opponent.extradata = opponent.extradata or {} + opponent.extradata.region = data.region + opponent.extradata.subregion = data.subregion + opponent.extradata.country = data.country - table.insert(standings, { + table.insert(standings,{ place = tonumber(key), points = tonumber(data.points), opponent = opponent }) end) - self._store(settings.updated, standings) + VRSStandings._store(settings.updated, settings.datapointType, standings) end Array.sortInPlaceBy(standings, Operator.property('place')) + if settings.filterType ~= 'none' then + for i, entry in ipairs(standings) do + entry.global_place = i + end + end + -- filtering + standings = Array.filter(standings, function(entry) + local extradata = entry.opponent.extradata or {} + + if settings.filterType == 'region' then + return extradata.region == settings.filterRegion + end + + if settings.filterType == 'subregion' then + return extradata.subregion == settings.filterSubregion + end + + if settings.filterType == 'country' then + local matchingPlayers = Array.filter(entry.opponent.players, function(player) + return player ~= nil + and player.flag ~= nil + and settings.filterCountries[player.flag] == true + end) + return #matchingPlayers >= 3 + end + + return true + end) + + if settings.fetchLimit then + standings = Array.sub(standings, 1, settings.fetchLimit) + end + + for i, entry in ipairs(standings) do + entry.place = i + end + return standings, settings end ---@private ----@param standing {place: number, points: number, opponent: standardOpponent} ----@return Widget -function VRSStandings._row(standing) - return TableWidgets.Row{children = WidgetUtil.collect( - TableWidgets.Cell{ - children = standing.place, - }, - TableWidgets.Cell{ - children = MathUtil.formatRounded{value = standing.points, precision = 1} - }, - TableWidgets.Cell{ - children = OpponentDisplay.InlineTeamContainer{ - template = standing.opponent.template +function VRSStandings._row(standing, mainpage) + local extradata = standing.opponent.extradata or {} + + local cells + if standing.global_place then + cells = WidgetUtil.collect( + TableWidgets.Cell{children = standing.place}, + TableWidgets.Cell{children = standing.global_place}, + TableWidgets.Cell{ + children = MathUtil.formatRounded{value = standing.points, precision = 1} + }, + TableWidgets.Cell{ + children = OpponentDisplay.InlineTeamContainer{ + template = standing.opponent.template + } } - }, - TableWidgets.Cell{ - children = Array.map(standing.opponent.players, function(player) - return HtmlWidgets.Span{ - css = { - display = "inline-block", - width = "160px" - }, - children = PlayerDisplay.InlinePlayer({player = player}) + ) + else + cells = WidgetUtil.collect( + TableWidgets.Cell{children = standing.place}, + TableWidgets.Cell{ + children = MathUtil.formatRounded{value = standing.points, precision = 1} + }, + TableWidgets.Cell{ + children = OpponentDisplay.InlineTeamContainer{ + template = standing.opponent.template } - end), - } - )} + }, + TableWidgets.Cell{children = extradata.region or ''} + ) + end + + if not mainpage then + table.insert(cells, + TableWidgets.Cell{ + children = Array.map(standing.opponent.players,function(player) + return HtmlWidgets.Span{ + css = {display="inline-block", width="160px"}, + children = PlayerDisplay.InlinePlayer({player = player}) + } + end) + } + ) + end + + return TableWidgets.Row{children = cells} end ---@private ----@param updated string ----@param standings {place: number, points: number, opponent: standardOpponent}[] -function VRSStandings._store(updated, standings) +function VRSStandings._store(updated, datapointType, standings) if Lpdb.isStorageDisabled() then return end + local dataPoint = Lpdb.DataPoint:new{ - objectname = 'vrs_' .. updated, - type = DATAPOINT_TYPE, + objectname = datapointType .. '_' .. updated, + type = datapointType, name = 'Inofficial VRS (' .. updated .. ')', date = updated, extradata = standings } + dataPoint:save() end ---@private ----@param updated string ----@param fetchLimit integer ----@return {place: number, points: number, opponent: standardOpponent}[] -function VRSStandings._fetch(updated, fetchLimit) +function VRSStandings._fetch(updated, datapointType) + local conditions = Condition.Tree(BooleanOperator.all):add{ + Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), + Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), + } + + if datapointType == DATAPOINT_TYPE_MAIN then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + else + conditions:add{ + Condition.Tree(BooleanOperator.any):add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIVE), + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + } + end + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { - conditions = Condition.Tree(BooleanOperator.all):add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE), - Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), - }:toString(), + conditions = conditions:toString(), query = 'extradata', limit = 1, }) - assert(data[1], 'No VRS data found') - return Array.sub(data[1].extradata, 1, fetchLimit) + assert(data[1], 'No VRS data found for type "' .. datapointType .. '" on date "' .. updated .. '"') + return data[1].extradata +end + +---@private +function VRSStandings._fetchLatestDate(datapointType) + local conditions = Condition.Tree(BooleanOperator.all):add{ + Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), + } + + if datapointType == DATAPOINT_TYPE_MAIN then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + else + conditions:add{ + Condition.Tree(BooleanOperator.any):add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIVE), + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + } + end + + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = conditions:toString(), + query = 'date', + order = 'date desc', + limit = 1, + }) + + assert(data[1], 'No VRS data found for type "' .. datapointType .. '"') + return DateExt.toYmdInUtc(DateExt.parseIsoDate(data[1].date)) end return VRSStandings From 9e982991a47d2807f7d5bb3fca089863b4362366 Mon Sep 17 00:00:00 2001 From: Finn Date: Sun, 22 Mar 2026 12:39:49 +0000 Subject: [PATCH 09/17] Update lua/wikis/counterstrike/VRSStandings.lua Co-authored-by: SyntacticSalt --- lua/wikis/counterstrike/VRSStandings.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index c051d73ceb5..9996e837738 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -44,10 +44,6 @@ local FOOTER_LINK = 'Valve_Regional_Standings' local VRSStandings = Class.new(Widget) VRSStandings.defaultProps = { title = 'VRS Standings', - filterRegion = nil, - filterSubregion = nil, - filterCountry = nil, - mainpage = nil, rankingType = 'live', } From 6b0ba4678648ed1b4eed885170f81ee3497e6c2f Mon Sep 17 00:00:00 2001 From: Finn Date: Sun, 22 Mar 2026 12:40:05 +0000 Subject: [PATCH 10/17] Update lua/wikis/counterstrike/MainPageLayout/data.lua Co-authored-by: SyntacticSalt --- lua/wikis/counterstrike/MainPageLayout/data.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/wikis/counterstrike/MainPageLayout/data.lua b/lua/wikis/counterstrike/MainPageLayout/data.lua index 525fd1f35f1..dcefcf8d9ff 100644 --- a/lua/wikis/counterstrike/MainPageLayout/data.lua +++ b/lua/wikis/counterstrike/MainPageLayout/data.lua @@ -78,9 +78,9 @@ local CONTENT = { vrsStandings = { heading = 'Valve Regional Standings', body = VRSStandings{ - shouldFetch = 1, + shouldFetch = true, fetchLimit = 5, - mainpage = 1, + mainpage = true, }, padding = false, boxid = 1521, From 219817274da4aa32fa315b39ac6e06083c37fae3 Mon Sep 17 00:00:00 2001 From: Finn Date: Sun, 22 Mar 2026 12:44:14 +0000 Subject: [PATCH 11/17] Apply suggestions from code review Co-authored-by: SyntacticSalt Co-authored-by: ElectricalBoy <15651807+ElectricalBoy@users.noreply.github.com> --- lua/wikis/counterstrike/VRSStandings.lua | 108 +++++++++-------------- 1 file changed, 44 insertions(+), 64 deletions(-) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index 9996e837738..b0a002e127d 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- page=Module:Widget/VRSStandings.lua +-- page=Module:Widget/VRSStandings -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -16,9 +16,9 @@ local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') local MathUtil = Lua.import('Module:MathUtil') local Operator = Lua.import('Module:Operator') -local Opponent = Lua.import('Module:Opponent') -local PlayerDisplay = Lua.import('Module:Player/Display') -local OpponentDisplay = Lua.import('Module:OpponentDisplay') +local Opponent = Lua.import('Module:Opponent/Custom') +local PlayerDisplay = Lua.import('Module:Player/Display/Custom') +local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') local Table = Lua.import('Module:Table') local TableWidgets = Lua.import('Module:Widget/Table2/All') @@ -185,7 +185,21 @@ function VRSStandings:render() end end +---@class VRSStandingsStanding +---@field place number +---@field points number +---@field local_place number? +---@field opponent standardOpponent + +---@class VRSStandingsSettings +---@field title string +---@field shouldFetch boolean +---@field mainpage boolean +---@field rankingType 'main' | 'live' + ---@private +---@return VRSStandingsStanding[] +---@return VRSStandingsSettings function VRSStandings:_parse() local props = self.props local rankingType = (props.rankingType == 'main') and 'main' or 'live' @@ -197,16 +211,16 @@ function VRSStandings:_parse() fetchLimit = tonumber(props.fetchLimit), filterRegion = props.filterRegion, filterSubregion = props.filterSubregion, - filterCountry = props.filterCountry, + filterCountry = Array.parseCommaSeparatedString(props.filterCountry), mainpage = Logic.readBool(props.mainpage), rankingType = rankingType, datapointType = datapointType, } - if props.updated == 'latest' or not props.updated then - settings.updated = VRSStandings._fetchLatestDate(datapointType) - else + if props.updated ~= 'latest' then settings.updated = DateExt.toYmdInUtc(props.updated) + else + assert(settings.shouldFetch, '\'Latest\' can only be used for fetching data') end -- Only one filter can be applied at once @@ -220,22 +234,8 @@ function VRSStandings:_parse() settings.filterType = 'country' end - settings.filterCountries = nil - settings.filterCountryDisplay = 'Filtered' - - if settings.filterCountry then - local rawList = mw.text.split(settings.filterCountry, ',') - local countrySet = {} - for _, raw in ipairs(rawList) do - countrySet[mw.text.trim(raw)] = true - end - - settings.filterCountries = countrySet - settings.filterCountryDisplay = #rawList > 1 and 'Filtered' or mw.text.trim(rawList[1]) - end - - ---@type {points: number, opponent: standardOpponent}[] + ---@type VRSStandingsStanding[] local standings = {} if settings.shouldFetch then @@ -272,11 +272,6 @@ function VRSStandings:_parse() Array.sortInPlaceBy(standings, Operator.property('place')) - if settings.filterType ~= 'none' then - for i, entry in ipairs(standings) do - entry.global_place = i - end - end -- filtering standings = Array.filter(standings, function(entry) local extradata = entry.opponent.extradata or {} @@ -305,14 +300,17 @@ function VRSStandings:_parse() standings = Array.sub(standings, 1, settings.fetchLimit) end - for i, entry in ipairs(standings) do - entry.place = i - end + Array.forEach(standings, function(entry, index) do + entry.local_place = index + end) return standings, settings end ---@private +---@param standing VRSStandingsStanding +---@param mainpage boolean +---@return Widget function VRSStandings._row(standing, mainpage) local extradata = standing.opponent.extradata or {} @@ -348,7 +346,7 @@ function VRSStandings._row(standing, mainpage) if not mainpage then table.insert(cells, TableWidgets.Cell{ - children = Array.map(standing.opponent.players,function(player) + children = Array.map(standing.opponent.players, function(player) return HtmlWidgets.Span{ css = {display="inline-block", width="160px"}, children = PlayerDisplay.InlinePlayer({player = player}) @@ -362,6 +360,9 @@ function VRSStandings._row(standing, mainpage) end ---@private +---@param updated string +---@param datapointType string +---@param standings VRSStandingsStanding[] function VRSStandings._store(updated, datapointType, standings) if Lpdb.isStorageDisabled() then return @@ -370,7 +371,7 @@ function VRSStandings._store(updated, datapointType, standings) local dataPoint = Lpdb.DataPoint:new{ objectname = datapointType .. '_' .. updated, type = datapointType, - name = 'Inofficial VRS (' .. updated .. ')', + name = 'Unofficial VRS (' .. updated .. ')', date = updated, extradata = standings } @@ -379,16 +380,22 @@ function VRSStandings._store(updated, datapointType, standings) end ---@private +---@param updated string +---@param datapointType string +---@return VRSStandingsStanding[] function VRSStandings._fetch(updated, datapointType) local conditions = Condition.Tree(BooleanOperator.all):add{ - Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), } + + if updated == 'latest' then + conditions.add(Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated)) + end if datapointType == DATAPOINT_TYPE_MAIN then - conditions:add{ + conditions:add( Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), - } + ) else conditions:add{ Condition.Tree(BooleanOperator.any):add{ @@ -401,6 +408,7 @@ function VRSStandings._fetch(updated, datapointType) local data = mw.ext.LiquipediaDB.lpdb('datapoint', { conditions = conditions:toString(), query = 'extradata', + order = 'date desc', limit = 1, }) @@ -409,33 +417,5 @@ function VRSStandings._fetch(updated, datapointType) end ---@private -function VRSStandings._fetchLatestDate(datapointType) - local conditions = Condition.Tree(BooleanOperator.all):add{ - Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), - } - - if datapointType == DATAPOINT_TYPE_MAIN then - conditions:add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), - } - else - conditions:add{ - Condition.Tree(BooleanOperator.any):add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIVE), - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), - } - } - end - - local data = mw.ext.LiquipediaDB.lpdb('datapoint', { - conditions = conditions:toString(), - query = 'date', - order = 'date desc', - limit = 1, - }) - - assert(data[1], 'No VRS data found for type "' .. datapointType .. '"') - return DateExt.toYmdInUtc(DateExt.parseIsoDate(data[1].date)) -end return VRSStandings From d00dd11863317bc27f719a0bcb6f69265c455cfe Mon Sep 17 00:00:00 2001 From: mischiefcs Date: Sun, 22 Mar 2026 15:02:42 +0000 Subject: [PATCH 12/17] resolving errors, functionality and adding two more data types --- .../{ => Widget}/VRSStandings.lua | 244 +++++++++++------- 1 file changed, 144 insertions(+), 100 deletions(-) rename lua/wikis/counterstrike/{ => Widget}/VRSStandings.lua (69%) diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/Widget/VRSStandings.lua similarity index 69% rename from lua/wikis/counterstrike/VRSStandings.lua rename to lua/wikis/counterstrike/Widget/VRSStandings.lua index b0a002e127d..b92bd300cbf 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/Widget/VRSStandings.lua @@ -33,9 +33,10 @@ local Comparator = Condition.Comparator local Link = Lua.import('Module:Widget/Basic/Link') local Icon = Lua.import('Module:Icon') - -local DATAPOINT_TYPE_LIVE = 'vrs_ranking_live' local DATAPOINT_TYPE_MAIN = 'vrs_ranking' +local DATAPOINT_TYPE_LIVE = 'vrs_ranking_live' +local DATAPOINT_TYPE_LIQUIPEDIA = 'vrs_ranking_liquipedia' +local DATAPOINT_TYPE_PREDICTION = 'vrs_ranking_prediction' local FOOTER_LINK = 'Valve_Regional_Standings' ---@class VRSStandings: Widget @@ -43,57 +44,88 @@ local FOOTER_LINK = 'Valve_Regional_Standings' ---@field props table local VRSStandings = Class.new(Widget) VRSStandings.defaultProps = { - title = 'VRS Standings', - rankingType = 'live', + title = 'VRS Standings', + datapointType = DATAPOINT_TYPE_LIVE, } ---@return Widget? -function VRSStandings:render() - local standings, settings = self:_parse() - - local headerCells +local function buildHeaderCells(settings) if settings.filterType ~= 'none' then - headerCells = WidgetUtil.collect( + local cells = WidgetUtil.collect( TableWidgets.CellHeader{children = 'Rank'}, TableWidgets.CellHeader{children = 'Global Rank'}, TableWidgets.CellHeader{children = 'Points'}, TableWidgets.CellHeader{children = 'Team'} ) + if not settings.mainpage then + table.insert(cells, TableWidgets.CellHeader{children = 'Roster'}) + end + return cells else - headerCells = WidgetUtil.collect( + local cells = WidgetUtil.collect( TableWidgets.CellHeader{children = 'Rank'}, TableWidgets.CellHeader{children = 'Points'}, TableWidgets.CellHeader{children = 'Team'}, TableWidgets.CellHeader{children = 'Region'} ) + if not settings.mainpage then + table.insert(cells, TableWidgets.CellHeader{children = 'Roster'}) + end + return cells end +end - if not settings.mainpage then - table.insert(headerCells, TableWidgets.CellHeader{children = 'Roster'}) - end - - local headerRow = TableWidgets.TableHeader{ +local function buildHeaderRow(settings) + return TableWidgets.TableHeader{ children = { - TableWidgets.Row{children = headerCells} + TableWidgets.Row{children = buildHeaderCells(settings)} } } +end + +local function buildColumns(settings) + local columns + if settings.filterType ~= 'none' then + columns = WidgetUtil.collect( + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'} + ) + else + columns = WidgetUtil.collect( + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'}, + {align = 'center'} + ) + end + if settings.mainpage then + for _, col in ipairs(columns) do + col.width = (100 / #columns) .. '%' + end + end + if not settings.mainpage then + table.insert(columns, {align = 'left'}) + end + return columns +end + +local function buildTitle(settings) local regionMap = { AS = 'Asia', AM = 'Americas', EU = 'Europe' } - local titleName = 'Global' - if settings.filterType == 'region' then titleName = regionMap[settings.filterRegion] or settings.filterRegion elseif settings.filterType == 'subregion' then titleName = 'Subregion' elseif settings.filterType == 'country' then - titleName = settings.filterCountryDisplay + titleName = settings.filterCountryDisplay or 'Country' end - - local title = HtmlWidgets.Div { + return HtmlWidgets.Div { children = { HtmlWidgets.Div { children = { @@ -111,35 +143,10 @@ function VRSStandings:render() }, classes = { 'ranking-table__top-row' }, } +end - local columns - if settings.filterType ~= 'none' then - columns = WidgetUtil.collect( - {align = 'center', sortType = 'number'}, - {align = 'center', sortType = 'number'}, - {align = 'center', sortType = 'number'}, - {align = 'left'} - ) - else - columns = WidgetUtil.collect( - {align = 'center', sortType = 'number'}, - {align = 'center', sortType = 'number'}, - {align = 'left'}, - {align = 'center'} - ) - end - - if settings.mainpage then - for _, col in ipairs(columns) do - col.width = (100 / #columns) .. '%' - end - end - - if not settings.mainpage then - table.insert(columns, {align = 'left'}) - end - - local footer = Link { +local function buildFooter() + return Link { link = FOOTER_LINK, linktype = 'internal', children = { @@ -149,6 +156,10 @@ function VRSStandings:render() } }, } +end + +function VRSStandings:render() + local standings, settings = self:_parse() if #standings == 0 then return HtmlWidgets.Div{ @@ -160,13 +171,13 @@ function VRSStandings:render() end local tableWidget = TableWidgets.Table{ - title = title, + title = buildTitle(settings), sortable = false, - columns = columns, - footer = settings.mainpage and footer or nil, + columns = buildColumns(settings), + footer = settings.mainpage and buildFooter() or nil, css = settings.mainpage and { width = '100%' } or nil, children = { - headerRow, + buildHeaderRow(settings), TableWidgets.TableBody{ children = Array.map(standings, function(entry) return VRSStandings._row(entry, settings.mainpage) @@ -189,57 +200,74 @@ end ---@field place number ---@field points number ---@field local_place number? +---@field global_place number? ---@field opponent standardOpponent ---@class VRSStandingsSettings ---@field title string ---@field shouldFetch boolean +---@field fetchLimit number? +---@field filterRegion string? +---@field filterSubregion string? +---@field filterCountry string[]? +---@field filterCountryDisplay string? +---@field filterType 'none' | 'region' | 'subregion' | 'country' ---@field mainpage boolean ----@field rankingType 'main' | 'live' +---@field datapointType string +---@field updated string ---@private ---@return VRSStandingsStanding[] ---@return VRSStandingsSettings -function VRSStandings:_parse() - local props = self.props - local rankingType = (props.rankingType == 'main') and 'main' or 'live' - local datapointType = (rankingType == 'main') and DATAPOINT_TYPE_MAIN or DATAPOINT_TYPE_LIVE - - local settings = { - title = props.title, - shouldFetch = Logic.readBool(props.shouldFetch), - fetchLimit = tonumber(props.fetchLimit), - filterRegion = props.filterRegion, - filterSubregion = props.filterSubregion, - filterCountry = Array.parseCommaSeparatedString(props.filterCountry), - mainpage = Logic.readBool(props.mainpage), - rankingType = rankingType, - datapointType = datapointType, - } - if props.updated ~= 'latest' then - settings.updated = DateExt.toYmdInUtc(props.updated) +function VRSStandings:_parse() + local props = self.props + local datapointType = props.datapointType or DATAPOINT_TYPE_LIVE + + local updated + if props.updated == 'latest' then + assert(Logic.readBool(props.shouldFetch), '\'Latest\' can only be used for fetching data') + updated = 'latest' + elseif props.updated then + updated = DateExt.toYmdInUtc(props.updated) else - assert(settings.shouldFetch, '\'Latest\' can only be used for fetching data') - end - - -- Only one filter can be applied at once - settings.filterType = 'none' + if Logic.readBool(props.shouldFetch) then + updated = 'latest' + else + error('A date must be provided when not fetching data') + end + end + + local settings = { + title = props.title, + shouldFetch = Logic.readBool(props.shouldFetch), + fetchLimit = tonumber(props.fetchLimit), + filterRegion = props.filterRegion, + filterSubregion = props.filterSubregion, + filterCountry = Array.parseCommaSeparatedString(props.filterCountry), + filterCountryDisplay = props.filterCountryDisplay, + mainpage = Logic.readBool(props.mainpage), + datapointType = datapointType, + updated = updated, + filterType = 'none', + } if settings.filterRegion then settings.filterType = 'region' elseif settings.filterSubregion then settings.filterType = 'subregion' - elseif settings.filterCountry then + elseif settings.filterCountry and #settings.filterCountry > 0 then settings.filterType = 'country' end - ---@type VRSStandingsStanding[] local standings = {} + if settings.shouldFetch then - standings = VRSStandings._fetch(settings.updated, settings.datapointType) + local fetchedStandings, fetchedDate = VRSStandings._fetch(settings.updated, settings.datapointType) + standings = fetchedStandings + settings.updated = string.sub(fetchedDate, 1, 10) or settings.updated else Table.iter.forEachPair(self.props, function(key, value) if not string.match(key, '^%d+$') then @@ -253,7 +281,7 @@ function VRSStandings:_parse() })) data[1] = nil - opponent.players = Array.map(Array.range(1,5), FnUtil.curry(Opponent.readPlayerArgs, data)) + opponent.players = Array.map(Array.range(1, 5), FnUtil.curry(Opponent.readPlayerArgs, data)) opponent.extradata = opponent.extradata or {} opponent.extradata.region = data.region @@ -285,10 +313,14 @@ function VRSStandings:_parse() end if settings.filterType == 'country' then + local filterSet = {} + for _, flag in ipairs(settings.filterCountry) do + filterSet[flag] = true + end local matchingPlayers = Array.filter(entry.opponent.players, function(player) return player ~= nil and player.flag ~= nil - and settings.filterCountries[player.flag] == true + and filterSet[player.flag] == true end) return #matchingPlayers >= 3 end @@ -300,8 +332,11 @@ function VRSStandings:_parse() standings = Array.sub(standings, 1, settings.fetchLimit) end - Array.forEach(standings, function(entry, index) do + Array.forEach(standings, function(entry, index) entry.local_place = index + if settings.filterType ~= 'none' then + entry.global_place = entry.place + end end) return standings, settings @@ -317,7 +352,7 @@ function VRSStandings._row(standing, mainpage) local cells if standing.global_place then cells = WidgetUtil.collect( - TableWidgets.Cell{children = standing.place}, + TableWidgets.Cell{children = standing.local_place}, TableWidgets.Cell{children = standing.global_place}, TableWidgets.Cell{ children = MathUtil.formatRounded{value = standing.points, precision = 1} @@ -383,39 +418,48 @@ end ---@param updated string ---@param datapointType string ---@return VRSStandingsStanding[] +---@return string function VRSStandings._fetch(updated, datapointType) local conditions = Condition.Tree(BooleanOperator.all):add{ Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), } - - if updated == 'latest' then - conditions.add(Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated)) + + if updated ~= 'latest' then + conditions:add{ + Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated) + } end if datapointType == DATAPOINT_TYPE_MAIN then - conditions:add( - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), - ) - else conditions:add{ - Condition.Tree(BooleanOperator.any):add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIVE), - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), - } + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN) + } + elseif datapointType == DATAPOINT_TYPE_LIQUIPEDIA then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIQUIPEDIA) + } + elseif datapointType == DATAPOINT_TYPE_PREDICTION then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_PREDICTION) } + else + conditions:add( + Condition.Util.anyOf( + Condition.ColumnName('type'), + {DATAPOINT_TYPE_LIVE, DATAPOINT_TYPE_MAIN} + ) + ) end - local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { conditions = conditions:toString(), - query = 'extradata', + query = 'extradata, date', order = 'date desc', limit = 1, }) assert(data[1], 'No VRS data found for type "' .. datapointType .. '" on date "' .. updated .. '"') - return data[1].extradata + return data[1].extradata, data[1].date end ----@private - return VRSStandings From af674b27bf5eab0685128764f4d7046ac73dba0e Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 24 Mar 2026 09:16:52 +0000 Subject: [PATCH 13/17] Apply suggestions from code review Co-authored-by: ElectricalBoy <15651807+ElectricalBoy@users.noreply.github.com> --- lua/wikis/counterstrike/Widget/VRSStandings.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/wikis/counterstrike/Widget/VRSStandings.lua b/lua/wikis/counterstrike/Widget/VRSStandings.lua index b92bd300cbf..e9922c7c2eb 100644 --- a/lua/wikis/counterstrike/Widget/VRSStandings.lua +++ b/lua/wikis/counterstrike/Widget/VRSStandings.lua @@ -358,8 +358,8 @@ function VRSStandings._row(standing, mainpage) children = MathUtil.formatRounded{value = standing.points, precision = 1} }, TableWidgets.Cell{ - children = OpponentDisplay.InlineTeamContainer{ - template = standing.opponent.template + children = OpponentDisplay.InlineOpponent{ + opponent = standing.opponent } } ) @@ -370,8 +370,8 @@ function VRSStandings._row(standing, mainpage) children = MathUtil.formatRounded{value = standing.points, precision = 1} }, TableWidgets.Cell{ - children = OpponentDisplay.InlineTeamContainer{ - template = standing.opponent.template + children = OpponentDisplay.InlineOpponent{ + opponent = standing.opponent } }, TableWidgets.Cell{children = extradata.region or ''} From 867bee81706dd85fc74bdd9d85cec879a10d2ab2 Mon Sep 17 00:00:00 2001 From: mischiefcs Date: Tue, 24 Mar 2026 10:13:08 +0000 Subject: [PATCH 14/17] fixing trailing commas in code review --- .../counterstrike/Widget/VRSStandings.lua | 64 ++++++------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/lua/wikis/counterstrike/Widget/VRSStandings.lua b/lua/wikis/counterstrike/Widget/VRSStandings.lua index e9922c7c2eb..0e23e1fde3a 100644 --- a/lua/wikis/counterstrike/Widget/VRSStandings.lua +++ b/lua/wikis/counterstrike/Widget/VRSStandings.lua @@ -50,29 +50,15 @@ VRSStandings.defaultProps = { ---@return Widget? local function buildHeaderCells(settings) - if settings.filterType ~= 'none' then - local cells = WidgetUtil.collect( - TableWidgets.CellHeader{children = 'Rank'}, - TableWidgets.CellHeader{children = 'Global Rank'}, - TableWidgets.CellHeader{children = 'Points'}, - TableWidgets.CellHeader{children = 'Team'} - ) - if not settings.mainpage then - table.insert(cells, TableWidgets.CellHeader{children = 'Roster'}) - end - return cells - else - local cells = WidgetUtil.collect( - TableWidgets.CellHeader{children = 'Rank'}, - TableWidgets.CellHeader{children = 'Points'}, - TableWidgets.CellHeader{children = 'Team'}, - TableWidgets.CellHeader{children = 'Region'} - ) - if not settings.mainpage then - table.insert(cells, TableWidgets.CellHeader{children = 'Roster'}) - end - return cells - end + local filtered = settings.filterType ~= 'none' + return WidgetUtil.collect( + TableWidgets.CellHeader{children = 'Rank'}, + filtered and TableWidgets.CellHeader{children = 'Global Rank'} or nil, + TableWidgets.CellHeader{children = 'Points'}, + TableWidgets.CellHeader{children = 'Team'}, + not filtered and TableWidgets.CellHeader{children = 'Region'} or nil, + not settings.mainpage and TableWidgets.CellHeader{children = 'Roster'} or nil + ) end local function buildHeaderRow(settings) @@ -84,29 +70,19 @@ local function buildHeaderRow(settings) end local function buildColumns(settings) - local columns - if settings.filterType ~= 'none' then - columns = WidgetUtil.collect( - {align = 'center', sortType = 'number'}, - {align = 'center', sortType = 'number'}, - {align = 'center', sortType = 'number'}, - {align = 'left'} - ) - else - columns = WidgetUtil.collect( - {align = 'center', sortType = 'number'}, - {align = 'center', sortType = 'number'}, - {align = 'left'}, - {align = 'center'} - ) - end + local filtered = settings.filterType ~= 'none' + local columns = WidgetUtil.collect( + {align = 'center', sortType = 'number'}, + filtered and {align = 'center', sortType = 'number'} or nil, + {align = 'center', sortType = 'number'}, + {align = 'left'}, + not filtered and {align = 'center'} or nil, + not settings.mainpage and {align = 'left'} or nil + ) if settings.mainpage then - for _, col in ipairs(columns) do + Array.forEach(columns, function(col) col.width = (100 / #columns) .. '%' - end - end - if not settings.mainpage then - table.insert(columns, {align = 'left'}) + end) end return columns end From 1e6d1cf6b90d8ddc96fa5f188e0f4f123beca879 Mon Sep 17 00:00:00 2001 From: mischiefcs Date: Tue, 24 Mar 2026 12:08:43 +0000 Subject: [PATCH 15/17] resolving review --- lua/wikis/counterstrike/Widget/VRSStandings.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lua/wikis/counterstrike/Widget/VRSStandings.lua b/lua/wikis/counterstrike/Widget/VRSStandings.lua index 0e23e1fde3a..2fd8817cdf3 100644 --- a/lua/wikis/counterstrike/Widget/VRSStandings.lua +++ b/lua/wikis/counterstrike/Widget/VRSStandings.lua @@ -195,7 +195,6 @@ end ---@private ---@return VRSStandingsStanding[] ---@return VRSStandingsSettings - function VRSStandings:_parse() local props = self.props local datapointType = props.datapointType or DATAPOINT_TYPE_LIVE @@ -293,10 +292,10 @@ function VRSStandings:_parse() for _, flag in ipairs(settings.filterCountry) do filterSet[flag] = true end - local matchingPlayers = Array.filter(entry.opponent.players, function(player) + local matchingPlayers = Array.filter(entry.opponent.players, function (player) return player ~= nil and player.flag ~= nil - and filterSet[player.flag] == true + and filterSet[player.flag] end) return #matchingPlayers >= 3 end @@ -428,11 +427,11 @@ function VRSStandings._fetch(updated, datapointType) end local data = mw.ext.LiquipediaDB.lpdb('datapoint', { - conditions = conditions:toString(), - query = 'extradata, date', - order = 'date desc', - limit = 1, - }) + conditions = conditions:toString(), + query = 'extradata, date', + order = 'date desc', + limit = 1, + }) assert(data[1], 'No VRS data found for type "' .. datapointType .. '" on date "' .. updated .. '"') return data[1].extradata, data[1].date From 26a64710ee384ef5367b8ce5e4571d743e5e1a62 Mon Sep 17 00:00:00 2001 From: mischiefcs Date: Thu, 26 Mar 2026 18:27:50 +0000 Subject: [PATCH 16/17] distributing code over two modules for widget and data --- lua/wikis/counterstrike/VRSStandingsData.lua | 251 +++++++++++++ .../counterstrike/Widget/VRSStandings.lua | 331 +++--------------- 2 files changed, 307 insertions(+), 275 deletions(-) create mode 100644 lua/wikis/counterstrike/VRSStandingsData.lua diff --git a/lua/wikis/counterstrike/VRSStandingsData.lua b/lua/wikis/counterstrike/VRSStandingsData.lua new file mode 100644 index 00000000000..bc1eade4ae4 --- /dev/null +++ b/lua/wikis/counterstrike/VRSStandingsData.lua @@ -0,0 +1,251 @@ +--- +-- @Liquipedia +-- page=Module:VRSStandingsData +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local DateExt = Lua.import('Module:Date/Ext') +local FnUtil = Lua.import('Module:FnUtil') +local Json = Lua.import('Module:Json') +local Logic = Lua.import('Module:Logic') +local Lpdb = Lua.import('Module:Lpdb') +local Operator = Lua.import('Module:Operator') +local Opponent = Lua.import('Module:Opponent/Custom') +local Table = Lua.import('Module:Table') + +local Condition = Lua.import('Module:Condition') +local BooleanOperator = Condition.BooleanOperator +local Comparator = Condition.Comparator + +local DATAPOINT_TYPE_MAIN = 'vrs_ranking' +local DATAPOINT_TYPE_LIVE = 'vrs_ranking_live' +local DATAPOINT_TYPE_LIQUIPEDIA = 'vrs_ranking_liquipedia' +local DATAPOINT_TYPE_PREDICTION = 'vrs_ranking_prediction' + +---@class VRSStandingsData +local VRSStandingsData = {} + +VRSStandingsData.DATAPOINT_TYPE_MAIN = DATAPOINT_TYPE_MAIN +VRSStandingsData.DATAPOINT_TYPE_LIVE = DATAPOINT_TYPE_LIVE +VRSStandingsData.DATAPOINT_TYPE_LIQUIPEDIA = DATAPOINT_TYPE_LIQUIPEDIA +VRSStandingsData.DATAPOINT_TYPE_PREDICTION = DATAPOINT_TYPE_PREDICTION + +---@class VRSStandingsStanding +---@field place number +---@field points number +---@field local_place number? +---@field global_place number? +---@field opponent standardOpponent + +---@class VRSStandingsSettings +---@field title string +---@field shouldFetch boolean +---@field fetchLimit number? +---@field filterRegion string? +---@field filterSubregion string? +---@field filterCountry string[]? +---@field filterCountryDisplay string? +---@field filterType 'none' | 'region' | 'subregion' | 'country' +---@field mainpage boolean +---@field datapointType string +---@field updated string + +---Parses props, fetches or reads inline data, stores if needed, applies +---filters, and returns the final standings list alongside resolved settings. +---@param props table +---@return VRSStandingsStanding[] +---@return VRSStandingsSettings +function VRSStandingsData.getStandings(props) + local datapointType = props.datapointType or DATAPOINT_TYPE_LIVE + + local updated + if props.updated == 'latest' then + assert(Logic.readBool(props.shouldFetch), '\'Latest\' can only be used for fetching data') + updated = 'latest' + elseif props.updated then + updated = DateExt.toYmdInUtc(props.updated) + else + if Logic.readBool(props.shouldFetch) then + updated = 'latest' + else + error('A date must be provided when not fetching data') + end + end + + ---@type VRSStandingsSettings + local settings = { + title = props.title, + shouldFetch = Logic.readBool(props.shouldFetch), + fetchLimit = tonumber(props.fetchLimit), + filterRegion = props.filterRegion, + filterSubregion = props.filterSubregion, + filterCountry = Array.parseCommaSeparatedString(props.filterCountry), + filterCountryDisplay = props.filterCountryDisplay, + mainpage = Logic.readBool(props.mainpage), + datapointType = datapointType, + updated = updated, + filterType = 'none', + } + + if settings.filterRegion then + settings.filterType = 'region' + elseif settings.filterSubregion then + settings.filterType = 'subregion' + elseif settings.filterCountry and #settings.filterCountry > 0 then + settings.filterType = 'country' + end + + ---@type VRSStandingsStanding[] + local standings = {} + + if settings.shouldFetch then + local fetchedStandings, fetchedDate = VRSStandingsData._fetch(settings.updated, settings.datapointType) + standings = fetchedStandings + settings.updated = string.sub(fetchedDate, 1, 10) or settings.updated + else + Table.iter.forEachPair(props, function(key, value) + if not string.match(key, '^%d+$') then + return + end + + local data = Json.parse(value) + + local opponent = Opponent.readOpponentArgs(Table.merge(data, { + type = Opponent.team, + })) + + data[1] = nil + opponent.players = Array.map(Array.range(1, 5), FnUtil.curry(Opponent.readPlayerArgs, data)) + + opponent.extradata = opponent.extradata or {} + opponent.extradata.region = data.region + opponent.extradata.subregion = data.subregion + opponent.extradata.country = data.country + + table.insert(standings, { + place = tonumber(key), + points = tonumber(data.points), + opponent = opponent + }) + end) + + VRSStandingsData._store(settings.updated, settings.datapointType, standings) + end + + Array.sortInPlaceBy(standings, Operator.property('place')) + + -- Filtering + standings = Array.filter(standings, function(entry) + local extradata = entry.opponent.extradata or {} + + if settings.filterType == 'region' then + return extradata.region == settings.filterRegion + end + + if settings.filterType == 'subregion' then + return extradata.subregion == settings.filterSubregion + end + + if settings.filterType == 'country' then + local filterSet = {} + for _, flag in ipairs(settings.filterCountry) do + filterSet[flag] = true + end + local matchingPlayers = Array.filter(entry.opponent.players, function(player) + return player ~= nil + and player.flag ~= nil + and filterSet[player.flag] + end) + return #matchingPlayers >= 3 + end + + return true + end) + + if settings.fetchLimit then + standings = Array.sub(standings, 1, settings.fetchLimit) + end + + Array.forEach(standings, function(entry, index) + entry.local_place = index + if settings.filterType ~= 'none' then + entry.global_place = entry.place + end + end) + + return standings, settings +end + +---@private +---@param updated string +---@param datapointType string +---@param standings VRSStandingsStanding[] +function VRSStandingsData._store(updated, datapointType, standings) + if Lpdb.isStorageDisabled() then + return + end + + local dataPoint = Lpdb.DataPoint:new{ + objectname = datapointType .. '_' .. updated, + type = datapointType, + name = 'Unofficial VRS (' .. updated .. ')', + date = updated, + extradata = standings + } + + dataPoint:save() +end + +---@private +---@param updated string +---@param datapointType string +---@return VRSStandingsStanding[] +---@return string +function VRSStandingsData._fetch(updated, datapointType) + local conditions = Condition.Tree(BooleanOperator.all):add{ + Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), + } + + if updated ~= 'latest' then + conditions:add{ + Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated) + } + end + + if datapointType == DATAPOINT_TYPE_MAIN then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN) + } + elseif datapointType == DATAPOINT_TYPE_LIQUIPEDIA then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIQUIPEDIA) + } + elseif datapointType == DATAPOINT_TYPE_PREDICTION then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_PREDICTION) + } + else + conditions:add( + Condition.Util.anyOf( + Condition.ColumnName('type'), + {DATAPOINT_TYPE_LIVE, DATAPOINT_TYPE_MAIN} + ) + ) + end + + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = conditions:toString(), + query = 'extradata, date', + order = 'date desc', + limit = 1, + }) + + assert(data[1], 'No VRS data found for type "' .. datapointType .. '" on date "' .. updated .. '"') + return data[1].extradata, data[1].date +end + +return VRSStandingsData diff --git a/lua/wikis/counterstrike/Widget/VRSStandings.lua b/lua/wikis/counterstrike/Widget/VRSStandings.lua index 2fd8817cdf3..3b370848306 100644 --- a/lua/wikis/counterstrike/Widget/VRSStandings.lua +++ b/lua/wikis/counterstrike/Widget/VRSStandings.lua @@ -9,34 +9,20 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') -local DateExt = Lua.import('Module:Date/Ext') -local FnUtil = Lua.import('Module:FnUtil') -local Json = Lua.import('Module:Json') -local Logic = Lua.import('Module:Logic') -local Lpdb = Lua.import('Module:Lpdb') local MathUtil = Lua.import('Module:MathUtil') -local Operator = Lua.import('Module:Operator') -local Opponent = Lua.import('Module:Opponent/Custom') local PlayerDisplay = Lua.import('Module:Player/Display/Custom') local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') -local Table = Lua.import('Module:Table') local TableWidgets = Lua.import('Module:Widget/Table2/All') local Widget = Lua.import('Module:Widget') local WidgetUtil = Lua.import('Module:Widget/Util') local HtmlWidgets = Lua.import('Module:Widget/Html/All') -local Condition = Lua.import('Module:Condition') -local BooleanOperator = Condition.BooleanOperator -local Comparator = Condition.Comparator - local Link = Lua.import('Module:Widget/Basic/Link') local Icon = Lua.import('Module:Icon') -local DATAPOINT_TYPE_MAIN = 'vrs_ranking' -local DATAPOINT_TYPE_LIVE = 'vrs_ranking_live' -local DATAPOINT_TYPE_LIQUIPEDIA = 'vrs_ranking_liquipedia' -local DATAPOINT_TYPE_PREDICTION = 'vrs_ranking_prediction' +local VRSStandingsData = Lua.import('Module:VRSStandingsData') + local FOOTER_LINK = 'Valve_Regional_Standings' ---@class VRSStandings: Widget @@ -44,11 +30,12 @@ local FOOTER_LINK = 'Valve_Regional_Standings' ---@field props table local VRSStandings = Class.new(Widget) VRSStandings.defaultProps = { - title = 'VRS Standings', - datapointType = DATAPOINT_TYPE_LIVE, + title = 'VRS Standings', + datapointType = VRSStandingsData.DATAPOINT_TYPE_LIVE, } ----@return Widget? +---@param settings VRSStandingsSettings +---@return Widget[] local function buildHeaderCells(settings) local filtered = settings.filterType ~= 'none' return WidgetUtil.collect( @@ -61,6 +48,8 @@ local function buildHeaderCells(settings) ) end +---@param settings VRSStandingsSettings +---@return Widget local function buildHeaderRow(settings) return TableWidgets.TableHeader{ children = { @@ -69,6 +58,8 @@ local function buildHeaderRow(settings) } end +---@param settings VRSStandingsSettings +---@return table[] local function buildColumns(settings) local filtered = settings.filterType ~= 'none' local columns = WidgetUtil.collect( @@ -87,6 +78,8 @@ local function buildColumns(settings) return columns end +---@param settings VRSStandingsSettings +---@return Widget local function buildTitle(settings) local regionMap = { AS = 'Asia', @@ -101,223 +94,40 @@ local function buildTitle(settings) elseif settings.filterType == 'country' then titleName = settings.filterCountryDisplay or 'Country' end - return HtmlWidgets.Div { + return HtmlWidgets.Div{ children = { - HtmlWidgets.Div { + HtmlWidgets.Div{ children = { - HtmlWidgets.B { children = 'Unofficial ' .. titleName .. ' VRS' }, - HtmlWidgets.Span { children = 'Last updated: ' .. settings.updated } + HtmlWidgets.B{children = 'Unofficial ' .. titleName .. ' VRS'}, + HtmlWidgets.Span{children = 'Last updated: ' .. settings.updated} }, - classes = { 'ranking-table__top-row-text' } + classes = {'ranking-table__top-row-text'} }, - HtmlWidgets.Div { + HtmlWidgets.Div{ children = { - HtmlWidgets.Span { children = 'Data by Liquipedia' }, + HtmlWidgets.Span{children = 'Data by Liquipedia'}, }, - classes = { 'ranking-table__top-row-logo-container' } + classes = {'ranking-table__top-row-logo-container'} } }, - classes = { 'ranking-table__top-row' }, + classes = {'ranking-table__top-row'}, } end +---@return Widget local function buildFooter() - return Link { + return Link{ link = FOOTER_LINK, linktype = 'internal', children = { - HtmlWidgets.Div { - children = { 'See Rankings Page', Icon.makeIcon { iconName = 'goto' } }, - classes = { 'ranking-table__footer-button' }, - } - }, - } -end - -function VRSStandings:render() - local standings, settings = self:_parse() - - if #standings == 0 then - return HtmlWidgets.Div{ - children = { - HtmlWidgets.B{ children = 'No teams found for the selected filter.' } - }, - css = { padding = '12px' } - } - end - - local tableWidget = TableWidgets.Table{ - title = buildTitle(settings), - sortable = false, - columns = buildColumns(settings), - footer = settings.mainpage and buildFooter() or nil, - css = settings.mainpage and { width = '100%' } or nil, - children = { - buildHeaderRow(settings), - TableWidgets.TableBody{ - children = Array.map(standings, function(entry) - return VRSStandings._row(entry, settings.mainpage) - end) + HtmlWidgets.Div{ + children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, + classes = {'ranking-table__footer-button'}, } }, } - - if settings.mainpage then - return HtmlWidgets.Div{ - css = { width = '100%' }, - children = { tableWidget } - } - else - return tableWidget - end -end - ----@class VRSStandingsStanding ----@field place number ----@field points number ----@field local_place number? ----@field global_place number? ----@field opponent standardOpponent - ----@class VRSStandingsSettings ----@field title string ----@field shouldFetch boolean ----@field fetchLimit number? ----@field filterRegion string? ----@field filterSubregion string? ----@field filterCountry string[]? ----@field filterCountryDisplay string? ----@field filterType 'none' | 'region' | 'subregion' | 'country' ----@field mainpage boolean ----@field datapointType string ----@field updated string - ----@private ----@return VRSStandingsStanding[] ----@return VRSStandingsSettings -function VRSStandings:_parse() - local props = self.props - local datapointType = props.datapointType or DATAPOINT_TYPE_LIVE - - local updated - if props.updated == 'latest' then - assert(Logic.readBool(props.shouldFetch), '\'Latest\' can only be used for fetching data') - updated = 'latest' - elseif props.updated then - updated = DateExt.toYmdInUtc(props.updated) - else - if Logic.readBool(props.shouldFetch) then - updated = 'latest' - else - error('A date must be provided when not fetching data') - end - end - - local settings = { - title = props.title, - shouldFetch = Logic.readBool(props.shouldFetch), - fetchLimit = tonumber(props.fetchLimit), - filterRegion = props.filterRegion, - filterSubregion = props.filterSubregion, - filterCountry = Array.parseCommaSeparatedString(props.filterCountry), - filterCountryDisplay = props.filterCountryDisplay, - mainpage = Logic.readBool(props.mainpage), - datapointType = datapointType, - updated = updated, - filterType = 'none', - } - - if settings.filterRegion then - settings.filterType = 'region' - elseif settings.filterSubregion then - settings.filterType = 'subregion' - elseif settings.filterCountry and #settings.filterCountry > 0 then - settings.filterType = 'country' - end - - ---@type VRSStandingsStanding[] - local standings = {} - - - if settings.shouldFetch then - local fetchedStandings, fetchedDate = VRSStandings._fetch(settings.updated, settings.datapointType) - standings = fetchedStandings - settings.updated = string.sub(fetchedDate, 1, 10) or settings.updated - else - Table.iter.forEachPair(self.props, function(key, value) - if not string.match(key, '^%d+$') then - return - end - - local data = Json.parse(value) - - local opponent = Opponent.readOpponentArgs(Table.merge(data, { - type = Opponent.team, - })) - - data[1] = nil - opponent.players = Array.map(Array.range(1, 5), FnUtil.curry(Opponent.readPlayerArgs, data)) - - opponent.extradata = opponent.extradata or {} - opponent.extradata.region = data.region - opponent.extradata.subregion = data.subregion - opponent.extradata.country = data.country - - table.insert(standings,{ - place = tonumber(key), - points = tonumber(data.points), - opponent = opponent - }) - end) - - VRSStandings._store(settings.updated, settings.datapointType, standings) - end - - Array.sortInPlaceBy(standings, Operator.property('place')) - - -- filtering - standings = Array.filter(standings, function(entry) - local extradata = entry.opponent.extradata or {} - - if settings.filterType == 'region' then - return extradata.region == settings.filterRegion - end - - if settings.filterType == 'subregion' then - return extradata.subregion == settings.filterSubregion - end - - if settings.filterType == 'country' then - local filterSet = {} - for _, flag in ipairs(settings.filterCountry) do - filterSet[flag] = true - end - local matchingPlayers = Array.filter(entry.opponent.players, function (player) - return player ~= nil - and player.flag ~= nil - and filterSet[player.flag] - end) - return #matchingPlayers >= 3 - end - - return true - end) - - if settings.fetchLimit then - standings = Array.sub(standings, 1, settings.fetchLimit) - end - - Array.forEach(standings, function(entry, index) - entry.local_place = index - if settings.filterType ~= 'none' then - entry.global_place = entry.place - end - end) - - return standings, settings end ----@private ---@param standing VRSStandingsStanding ---@param mainpage boolean ---@return Widget @@ -369,72 +179,43 @@ function VRSStandings._row(standing, mainpage) return TableWidgets.Row{children = cells} end ----@private ----@param updated string ----@param datapointType string ----@param standings VRSStandingsStanding[] -function VRSStandings._store(updated, datapointType, standings) - if Lpdb.isStorageDisabled() then - return - end - - local dataPoint = Lpdb.DataPoint:new{ - objectname = datapointType .. '_' .. updated, - type = datapointType, - name = 'Unofficial VRS (' .. updated .. ')', - date = updated, - extradata = standings - } - - dataPoint:save() -end - ----@private ----@param updated string ----@param datapointType string ----@return VRSStandingsStanding[] ----@return string -function VRSStandings._fetch(updated, datapointType) - local conditions = Condition.Tree(BooleanOperator.all):add{ - Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), - } +---@return Widget +function VRSStandings:render() + local standings, settings = VRSStandingsData.getStandings(self.props) - if updated ~= 'latest' then - conditions:add{ - Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated) + if #standings == 0 then + return HtmlWidgets.Div{ + children = { + HtmlWidgets.B{children = 'No teams found for the selected filter.'} + }, + css = {padding = '12px'} } end - if datapointType == DATAPOINT_TYPE_MAIN then - conditions:add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN) - } - elseif datapointType == DATAPOINT_TYPE_LIQUIPEDIA then - conditions:add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIQUIPEDIA) - } - elseif datapointType == DATAPOINT_TYPE_PREDICTION then - conditions:add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_PREDICTION) + local tableWidget = TableWidgets.Table{ + title = buildTitle(settings), + sortable = false, + columns = buildColumns(settings), + footer = settings.mainpage and buildFooter() or nil, + css = settings.mainpage and {width = '100%'} or nil, + children = { + buildHeaderRow(settings), + TableWidgets.TableBody{ + children = Array.map(standings, function(entry) + return VRSStandings._row(entry, settings.mainpage) + end) + } + }, + } + + if settings.mainpage then + return HtmlWidgets.Div{ + css = {width = '100%'}, + children = {tableWidget} } else - conditions:add( - Condition.Util.anyOf( - Condition.ColumnName('type'), - {DATAPOINT_TYPE_LIVE, DATAPOINT_TYPE_MAIN} - ) - ) + return tableWidget end - - local data = mw.ext.LiquipediaDB.lpdb('datapoint', { - conditions = conditions:toString(), - query = 'extradata, date', - order = 'date desc', - limit = 1, - }) - - assert(data[1], 'No VRS data found for type "' .. datapointType .. '" on date "' .. updated .. '"') - return data[1].extradata, data[1].date end return VRSStandings From 108ab4bfbc5b0c2ee49045f5a93d2f9dfb48b2a2 Mon Sep 17 00:00:00 2001 From: mischiefcs Date: Fri, 27 Mar 2026 16:09:20 +0000 Subject: [PATCH 17/17] removing mainpage sizing & allowing custom filter name for subregion --- lua/wikis/counterstrike/VRSStandingsData.lua | 4 ++-- lua/wikis/counterstrike/Widget/VRSStandings.lua | 14 +++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/lua/wikis/counterstrike/VRSStandingsData.lua b/lua/wikis/counterstrike/VRSStandingsData.lua index bc1eade4ae4..c15a8adecc1 100644 --- a/lua/wikis/counterstrike/VRSStandingsData.lua +++ b/lua/wikis/counterstrike/VRSStandingsData.lua @@ -48,7 +48,7 @@ VRSStandingsData.DATAPOINT_TYPE_PREDICTION = DATAPOINT_TYPE_PREDICTION ---@field filterRegion string? ---@field filterSubregion string? ---@field filterCountry string[]? ----@field filterCountryDisplay string? +---@field filterDisplayName string? ---@field filterType 'none' | 'region' | 'subregion' | 'country' ---@field mainpage boolean ---@field datapointType string @@ -84,7 +84,7 @@ function VRSStandingsData.getStandings(props) filterRegion = props.filterRegion, filterSubregion = props.filterSubregion, filterCountry = Array.parseCommaSeparatedString(props.filterCountry), - filterCountryDisplay = props.filterCountryDisplay, + filterDisplayName = props.filterDisplayName, mainpage = Logic.readBool(props.mainpage), datapointType = datapointType, updated = updated, diff --git a/lua/wikis/counterstrike/Widget/VRSStandings.lua b/lua/wikis/counterstrike/Widget/VRSStandings.lua index 3b370848306..44bc4a4ace3 100644 --- a/lua/wikis/counterstrike/Widget/VRSStandings.lua +++ b/lua/wikis/counterstrike/Widget/VRSStandings.lua @@ -90,9 +90,9 @@ local function buildTitle(settings) if settings.filterType == 'region' then titleName = regionMap[settings.filterRegion] or settings.filterRegion elseif settings.filterType == 'subregion' then - titleName = 'Subregion' + titleName = settings.filterDisplayName or 'Subregion' elseif settings.filterType == 'country' then - titleName = settings.filterCountryDisplay or 'Country' + titleName = settings.filterDisplayName or 'Country' end return HtmlWidgets.Div{ children = { @@ -207,15 +207,7 @@ function VRSStandings:render() } }, } - - if settings.mainpage then - return HtmlWidgets.Div{ - css = {width = '100%'}, - children = {tableWidget} - } - else - return tableWidget - end + return tableWidget end return VRSStandings