Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a33cf6f
Import from Commons
hjpalpha Feb 10, 2026
0dead47
deploy header
ElectricalBoy Feb 10, 2026
a1c9594
use Lua.import
ElectricalBoy Feb 10, 2026
e71e90c
type annotations
ElectricalBoy Feb 10, 2026
fa7295a
clean up import
ElectricalBoy Feb 10, 2026
8bd4106
kick redundant helper function
ElectricalBoy Feb 10, 2026
cf57994
clean up condition building
ElectricalBoy Feb 10, 2026
47e04cc
type annotations
ElectricalBoy Feb 10, 2026
5f59ca8
allow both string and condition node for query
ElectricalBoy Feb 10, 2026
eca7221
make filterTournament function optional
ElectricalBoy Feb 10, 2026
09a4903
use StandardTournament
ElectricalBoy Feb 10, 2026
bab5131
clean up condition building
ElectricalBoy Feb 10, 2026
6d9b0de
remove unused helper function
ElectricalBoy Feb 10, 2026
cbdbeb9
update key
ElectricalBoy Feb 10, 2026
dc21c58
fix lpdb field name
ElectricalBoy Feb 10, 2026
c8effea
clean up player query
ElectricalBoy Feb 10, 2026
1eedb89
clean up unused param
ElectricalBoy Feb 10, 2026
e57e86c
refactor header
ElectricalBoy Feb 10, 2026
7a9f0b1
finish rest
ElectricalBoy Feb 10, 2026
24d997d
reverse order
ElectricalBoy Feb 10, 2026
57622e6
clean up query link
ElectricalBoy Feb 10, 2026
9b08e02
use DataTable widget
ElectricalBoy Feb 10, 2026
7080e06
use widget for header
ElectricalBoy Feb 10, 2026
a5fa6fa
Array.forEach
ElectricalBoy Feb 10, 2026
d98247d
use widget for footer
ElectricalBoy Feb 10, 2026
59e6a2b
remove unused import
ElectricalBoy Feb 10, 2026
e266ef9
underscore
ElectricalBoy Feb 10, 2026
70b7ac8
add form existence check
ElectricalBoy Feb 10, 2026
4359519
cleaning
ElectricalBoy Feb 10, 2026
bfcd5f5
store shortname in standardtournament
ElectricalBoy Feb 10, 2026
79a278c
use shortname
ElectricalBoy Feb 10, 2026
1981ab4
use Logic.readBool
ElectricalBoy Feb 10, 2026
41b01db
lint
ElectricalBoy Feb 10, 2026
6711dd0
nil check
ElectricalBoy Feb 10, 2026
d3a995b
add classes when there is no context
ElectricalBoy Feb 25, 2026
56ede86
use table2
ElectricalBoy Feb 25, 2026
eb5f326
cleanup
ElectricalBoy Feb 25, 2026
5010b6f
use placement badge
ElectricalBoy Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lua/wikis/commons/MatchGroup/Util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ MatchGroupUtil.types.Game = TypeUtil.struct({
---@field resultType string?
---@field section string?
---@field series string?
---@field shortname string?
---@field status MatchStatus
---@field stream table
---@field tickername string?
Expand Down Expand Up @@ -313,6 +314,7 @@ MatchGroupUtil.types.Match = TypeUtil.struct({
resultType = 'string?',
section = 'string?',
series = 'string?',
shortname = 'string?',
status = MatchGroupUtil.types.Status,
stream = 'table',
tickername = 'string?',
Expand Down Expand Up @@ -587,6 +589,7 @@ function MatchGroupUtil.matchFromRecord(record)
resultType = nilIfEmpty(record.resulttype),
section = nilIfEmpty(record.section),
series = nilIfEmpty(record.series),
shortname = nilIfEmpty(record.shortname),
status = nilIfEmpty(record.status),
stream = Json.parseIfString(record.stream) or {},
tickername = record.tickername,
Expand Down
322 changes: 322 additions & 0 deletions lua/wikis/commons/PlayerTournamentAppearances.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
---
-- @Liquipedia
-- page=Module:PlayerTournamentAppearances
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Lua = require('Module:Lua')

local Arguments = Lua.import('Module:Arguments')
local Array = Lua.import('Module:Array')
local Class = Lua.import('Module:Class')
local Condition = Lua.import('Module:Condition')
local DateExt = Lua.import('Module:Date/Ext')
local Flags = Lua.import('Module:Flags')
local FnUtil = Lua.import('Module:FnUtil')
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 OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom')
local Page = Lua.import('Module:Page')
local Placement = Lua.import('Module:Placement')
local PlayerDisplay = Lua.import('Module:Player/Display/Custom')
local Table = Lua.import('Module:Table')
local Tournament = Lua.import('Module:Tournament')

local ConditionTree = Condition.Tree
local ConditionNode = Condition.Node
local Comparator = Condition.Comparator
local BooleanOperator = Condition.BooleanOperator
local ColumnName = Condition.ColumnName
local ConditionUtil = Condition.Util

local HtmlWidgets = Lua.import('Module:Widget/Html/All')
local LinkWidget = Lua.import('Module:Widget/Basic/Link')
local TableWidgets = Lua.import('Module:Widget/Table2/All')
local TournamentTitle = Lua.import('Module:Widget/Tournament/Title')
local WidgetUtil = Lua.import('Module:Widget/Util')

local DEFAULT_TIERTYPES = {'General', 'School', ''}
local FORM_NAME = 'Player tournament appearances'

---@class PlayerTournamentAppearances: BaseClass
---@operator call(Frame): PlayerTournamentAppearances
---@field tournaments StandardTournament[]
local Appearances = Class.new(function(self, frame) self:init(frame) end)

---@param frame Frame
---@return string|Widget
function Appearances.run(frame)
return Appearances(frame):create():build()
end

---@param frame Frame
---@return self
function Appearances:init(frame)
local args = Arguments.getArgs(frame)

self.plainArgs = args

assert(
args.series or args.pages or args.conditions,
'Either "series", "pages" or "conditions" input has to be specified'
)

self.config = {
showPlacementInsteadOfTeam = Logic.readBool(args.showPlacementInsteadOfTeam),
limit = tonumber(args.limit),
isFormQuery = Logic.readBool(args.query),
restrictToPlayersParticipatingIn = args.playerspage,
restrictToFirstPrizePool = Logic.readBool(args.restrictToFirstPrizePool),
}

self.args = {
conditions = args.conditions,
tierTypes = Logic.emptyOr(Array.parseCommaSeparatedString(args.tierTypes), DEFAULT_TIERTYPES),
tiers = Array.parseCommaSeparatedString(args.tiers),
startDate = Logic.nilIfEmpty(args.sdate),
endDate = Logic.nilIfEmpty(args.edate),
pages = Array.parseCommaSeparatedString(args.pages),
series = args.series and Array.map(
Array.extractValues(Table.filterByKey(args, function(key) return key:find('^series%d-$') end)),
Page.pageifyLink
) or nil,
}

return self
end

---@return self
function Appearances:create()
self.tournaments = Array.reverse(Tournament.getAllTournaments(self.args.conditions or self:_buildConditions()))

if Table.isEmpty(self.tournaments) then
return self
end

local pageNames = Array.map(self.tournaments, Operator.property('pageName'))
self.players = self:_fetchPlayers(pageNames)

return self
end

---@private
---@return ConditionTree
function Appearances:_buildConditions()
local args = self.args

local conditions = ConditionTree(BooleanOperator.all)
:add{ConditionNode(ColumnName('enddate'), Comparator.gt, DateExt.defaultDate)}

conditions:add(ConditionUtil.anyOf(ColumnName('status'), {'finished', ''}))
conditions:add(ConditionUtil.anyOf(ColumnName('liquipediatier'), args.tiers))
conditions:add(ConditionUtil.anyOf(ColumnName('liquipediatiertype'), args.tierTypes))

if Table.isNotEmpty(args.series) then
conditions:add(ConditionTree(BooleanOperator.any):add{
ConditionUtil.anyOf(ColumnName('seriespage'), args.series),
ConditionUtil.anyOf(ColumnName('series2', 'extradata'), args.series),
})
else
conditions:add(ConditionUtil.anyOf(ColumnName('pagename'), Array.map(args.pages, Page.pageifyLink)))
end

if args.startDate then
conditions:add(ConditionNode(ColumnName('startdate'), Comparator.ge, args.startDate))
end

if args.endDate then
conditions:add(ConditionNode(ColumnName('enddate'), Comparator.le, args.endDate))
end

return conditions
end

---@private
---@param pageNames string[]
---@return standardPlayer[]
function Appearances:_fetchPlayers(pageNames)
---@type table<string, standardPlayer>
local players = {}

Lpdb.executeMassQuery('placement', {
conditions = tostring(self:_placementConditions(pageNames)),
limit = 1000,
order = 'date asc',
query = 'opponentplayers, opponenttype, opponentname, parent, date, placement, opponenttemplate',
}, function (placement)
local opponent = Opponent.fromLpdbStruct(placement)
Array.forEach(opponent.players, function (player)
if Opponent.playerIsTbd(player) then
return
end
local pageName = player.pageName
---@cast pageName -nil
if not players[pageName] then
players[pageName] = player
player.extradata = player.extradata or {}
player.extradata.appearances = 0
player.extradata.placementSum = 0
player.extradata.results = {}
end

local extradata = players[pageName].extradata --[[ @as table ]]

extradata.appearances = extradata.appearances + 1
extradata.results[placement.parent] = {
placement = placement.placement,
date = placement.date,
team = placement.opponenttype == Opponent.team and placement.opponenttemplate or player.team
}
local rawPlacement = Placement.raw(placement.placement)
extradata.placementSum = extradata.placementSum + (tonumber(rawPlacement.placement[1]) or 1000)
end)
end)

local playersArray = Array.extractValues(players)

if self.config.restrictToPlayersParticipatingIn then
playersArray = Array.filter(playersArray, function(player)
return player.extradata.results[self.config.restrictToPlayersParticipatingIn]
end)
end

return Array.sortBy(
playersArray,
FnUtil.identity,
---@param a standardPlayer
---@param b standardPlayer
---@return boolean
function (a, b)
local aData = a.extradata or {}
local bData = b.extradata or {}
if aData.appearances ~= bData.appearances then
return aData.appearances > bData.appearances
elseif aData.placementSum ~= bData.placementSum then
return aData.placementSum < bData.placementSum
end
return a.pageName < b.pageName
end
)
end

---@private
---@param pageNames string[]
---@return ConditionTree
function Appearances:_placementConditions(pageNames)
local conditions = ConditionTree(BooleanOperator.all):add{
ConditionUtil.noneOf(ColumnName('opponentplayers'), {'', '[]'}),
ConditionUtil.noneOf(ColumnName('opponentname'), {'TBD', 'Definitions', ''}),
ConditionNode(ColumnName('mode'), Comparator.neq, 'award_individual'),
ConditionUtil.anyOf(ColumnName('parent'), pageNames)
}

if self.config.restrictToFirstPrizePool then
conditions:add(ConditionNode(ColumnName('prizepoolindex'), Comparator.eq, 1))
end

return conditions
end

---@return string|Widget
function Appearances:build()
if not self.players then return 'No results found.' end

local limit = math.min(self.config.limit or #self.players, #self.players)

return TableWidgets.Table{
sortable = true,
children = WidgetUtil.collect(
self:_header(),
TableWidgets.TableBody{
children = Array.map(Array.range(1, limit), FnUtil.curry(Appearances._row, self))
}
),
footer = self:_buildQueryLink()
}
end

---@private
---@return Widget
function Appearances:_header()
return TableWidgets.TableHeader{children = {
TableWidgets.Row{children = WidgetUtil.collect(
TableWidgets.CellHeader{},
TableWidgets.CellHeader{children = 'Player'},
TableWidgets.CellHeader{children = HtmlWidgets.Abbr{children = 'TA.', title = 'Total appearances'}},
Array.map(self.tournaments, function (tournament)
return TableWidgets.CellHeader{children = TournamentTitle{
tournament = tournament, useShortName = true
}}
end)
)}
}}
end

---@private
---@param playerIndex integer
---@return Html
function Appearances:_row(playerIndex)
local player = self.players[playerIndex]

return TableWidgets.Row{children = WidgetUtil.collect(
TableWidgets.Cell{children = Flags.Icon{flag = player.flag}},
TableWidgets.Cell{children = PlayerDisplay.InlinePlayer{
player = player, showFlag = false
}},
TableWidgets.Cell{children = player.extradata.appearances},
Array.map(self.tournaments, function (tournament)
local result = player.extradata.results[tournament.pageName] or {}
if self.config.showPlacementInsteadOfTeam then
local rawPlacement = Placement.raw(result.placement or '')
return TableWidgets.Cell{
attributes = {
['data-sort-value'] = rawPlacement.sort
},
classes = {rawPlacement.backgroundClass},
children = Placement.renderInWidget{placement = result.placement}
}
elseif Logic.isNotEmpty(result) then
return TableWidgets.Cell{
align = 'center',
classes = tonumber(result.placement) == 1 and {'tournament-highlighted-bg'} or nil,
attributes = {['data-sort-value'] = result.team},
children = result.team and OpponentDisplay.InlineTeamContainer{
template = result.team, date = result.date, style = 'icon'
} or nil
}
end
return TableWidgets.Cell{}
end)
)}
end

---@private
---@return Widget?
function Appearances:_buildQueryLink()
if not self.config.restrictToPlayersParticipatingIn or self.config.isFormQuery then
return
elseif not Page.exists('Form:' .. FORM_NAME) then
return
end
local queryTable = {
['PTA[series]'] = self.plainArgs.series or '',
['PTA[pages]'] = table.concat(self.args.pages, ','),
['PTA[tiers]'] = self.plainArgs.tiers,
['PTA[limit]'] = self.plainArgs.limit or '',
['PTA[playerspage]'] = self.plainArgs.playerspage or '',
['PTA[query]'] = 'true',
pfRunQueryFormName = FORM_NAME,
wpRunQuery = 'Run query',
}

return LinkWidget{
link = tostring(mw.uri.fullUrl('Special:RunQuery/' .. FORM_NAME, queryTable)),
children = 'Click here to modify this table',
linktype = 'external',
}
end

return Appearances
9 changes: 6 additions & 3 deletions lua/wikis/commons/Tournament.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ local TOURNAMENT_PHASE = {

---@class StandardTournamentPartial
---@field displayName string
---@field shortName string?
---@field fullName string
---@field pageName string
---@field icon string?
Expand All @@ -47,15 +48,15 @@ local TOURNAMENT_PHASE = {
---@field extradata table
---@field isHighlighted fun(self: StandardTournament, options?: table): boolean

---@param conditions ConditionTree?
---@param filterTournament fun(tournament: StandardTournament): boolean
---@param conditions string|AbstractConditionNode?
---@param filterTournament? fun(tournament: StandardTournament): boolean
---@return StandardTournament[]
function Tournament.getAllTournaments(conditions, filterTournament)
local tournaments = {}
Lpdb.executeMassQuery(
'tournament',
{
conditions = conditions and conditions:toString() or nil,
conditions = conditions and tostring(conditions),
order = 'sortdate desc',
limit = 1000,
},
Expand Down Expand Up @@ -101,6 +102,7 @@ function Tournament.partialTournamentFromMatch(match)
---@type StandardTournamentPartial
return {
displayName = Logic.emptyOr(match.tickername, match.tournament) or (match.parent or ''):gsub('_', ' '),
shortName = match.shortname,
fullName = match.tournament,
pageName = match.parent,
liquipediaTier = Tier.toIdentifier(match.liquipediatier),
Expand All @@ -122,6 +124,7 @@ function Tournament.tournamentFromRecord(record)

local tournament = {
displayName = Logic.emptyOr(record.tickername, record.name) or record.pagename:gsub('_', ' '),
shortName = Logic.nilIfEmpty(record.shortname),
fullName = record.name,
pageName = record.pagename,
startDate = startDate,
Expand Down
1 change: 1 addition & 0 deletions lua/wikis/commons/Widget/Table2/Cell.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function Table2Cell:render()
props.shrink,
props.attributes
),
classes = props.classes,
children = props.children,
}
end
Expand Down
Loading
Loading