diff --git a/lua/wikis/commons/MatchGroup/Util.lua b/lua/wikis/commons/MatchGroup/Util.lua index c48ec414700..21a3e75b87d 100644 --- a/lua/wikis/commons/MatchGroup/Util.lua +++ b/lua/wikis/commons/MatchGroup/Util.lua @@ -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? @@ -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?', @@ -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, diff --git a/lua/wikis/commons/PlayerTournamentAppearances.lua b/lua/wikis/commons/PlayerTournamentAppearances.lua new file mode 100644 index 00000000000..2cc21452cfa --- /dev/null +++ b/lua/wikis/commons/PlayerTournamentAppearances.lua @@ -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 + 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 diff --git a/lua/wikis/commons/Tournament.lua b/lua/wikis/commons/Tournament.lua index caf2eb2b469..9409bb30ed6 100644 --- a/lua/wikis/commons/Tournament.lua +++ b/lua/wikis/commons/Tournament.lua @@ -27,6 +27,7 @@ local TOURNAMENT_PHASE = { ---@class StandardTournamentPartial ---@field displayName string +---@field shortName string? ---@field fullName string ---@field pageName string ---@field icon string? @@ -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, }, @@ -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), @@ -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, diff --git a/lua/wikis/commons/Widget/Table2/Cell.lua b/lua/wikis/commons/Widget/Table2/Cell.lua index ddf6036748b..2b5bd267f50 100644 --- a/lua/wikis/commons/Widget/Table2/Cell.lua +++ b/lua/wikis/commons/Widget/Table2/Cell.lua @@ -53,6 +53,7 @@ function Table2Cell:render() props.shrink, props.attributes ), + classes = props.classes, children = props.children, } end diff --git a/lua/wikis/commons/Widget/Tournament/Title.lua b/lua/wikis/commons/Widget/Tournament/Title.lua index 7ada66e830b..23e81605b04 100644 --- a/lua/wikis/commons/Widget/Tournament/Title.lua +++ b/lua/wikis/commons/Widget/Tournament/Title.lua @@ -10,6 +10,7 @@ local Lua = require('Module:Lua') local Class = Lua.import('Module:Class') local Game = Lua.import('Module:Game') local LeagueIcon = Lua.import('Module:LeagueIcon') +local Logic = Lua.import('Module:Logic') local WidgetUtil = Lua.import('Module:Widget/Util') local Widget = Lua.import('Module:Widget') @@ -19,6 +20,7 @@ local Link = Lua.import('Module:Widget/Basic/Link') ---@class TournamentTitleProps ---@field tournament StandardTournamentPartial ---@field displayGameIcon boolean? +---@field useShortName boolean? ---@class TournamentTitleWidget: Widget ---@operator call(TournamentTitleProps): TournamentTitleWidget @@ -56,9 +58,7 @@ function TournamentTitleWidget:render() children = { Link{ link = tournament.pageName, - children = { - tournament.displayName, - }, + children = Logic.readBool(self.props.useShortName) and tournament.shortName or tournament.displayName, }, } }