diff --git a/lua/wikis/commons/AutomaticPointsTable.lua b/lua/wikis/commons/AutomaticPointsTable.lua index d7408596aae..e299f795023 100644 --- a/lua/wikis/commons/AutomaticPointsTable.lua +++ b/lua/wikis/commons/AutomaticPointsTable.lua @@ -11,12 +11,17 @@ 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 TableDisplay = Lua.import('Module:AutomaticPointsTable/Display') -local MinifiedDisplay = Lua.import('Module:AutomaticPointsTable/MinifiedDisplay') +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 String = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') +local TeamTemplate = Lua.import('Module:TeamTemplate') +local Tournament = Lua.import('Module:Tournament') local ConditionTree = Condition.Tree local ConditionNode = Condition.Node @@ -24,12 +29,38 @@ local Comparator = Condition.Comparator local BooleanOperator = Condition.BooleanOperator local ColumnName = Condition.ColumnName +local AutomaticPointsTableWidget = Lua.import('Module:Widget/AutomaticPointsTable') + +---@enum PointsType local POINTS_TYPE = { MANUAL = 'MANUAL', PRIZE = 'PRIZE', SECURED = 'SECURED' } +---@class AutomaticPointsTableConfig +---@field positionBackgrounds string[] +---@field tournaments StandardTournament[] +---@field opponents AutomaticPointsTableOpponent[] +---@field shouldTableBeMinified boolean +---@field limit number +---@field lpdbName string + +---@class AutomaticPointsTableOpponent +---@field opponent standardOpponent +---@field aliases string[][] +---@field tiebreakerPoints number +---@field results {type: PointsType?, amount: number?, qualified: boolean?, deduction: number?, note: string?}[] +---@field qualified boolean +---@field totalPoints number +---@field placement integer +---@field background string? +---@field note string? + +---@class AutomaticPointsTable +---@operator call(Frame): AutomaticPointsTable +---@field args table +---@field parsedInput AutomaticPointsTableConfig local AutomaticPointsTable = Class.new( function(self, frame) self.frame = frame @@ -38,85 +69,61 @@ local AutomaticPointsTable = Class.new( end ) +---@param frame Frame +---@return Widget function AutomaticPointsTable.run(frame) - local pointsTable = AutomaticPointsTable(frame) - - local teams = pointsTable.parsedInput.teams - local tournaments = pointsTable.parsedInput.tournaments - local teamsWithResults, tournamentsWithResults = pointsTable:queryPlacements(teams, tournaments) - local pointsData = pointsTable:getPointsData(teamsWithResults, tournamentsWithResults) - local sortedData = pointsTable:sortData(pointsData) - local sortedDataWithPositions = pointsTable:addPositionData(sortedData) - local positionBackgrounds = pointsTable.parsedInput.positionBackgrounds - local limit = pointsTable.parsedInput.limit - - -- A display module is a module that takes in 3 arguments and returns some html, - -- which will be displayed when this module is invoked - local usedDisplayModule - if pointsTable.parsedInput.shouldTableBeMinified then - usedDisplayModule = MinifiedDisplay - else - usedDisplayModule = TableDisplay - end - - pointsTable:storeLPDB(sortedDataWithPositions) - - local divTable = usedDisplayModule( - sortedDataWithPositions, - tournamentsWithResults, - positionBackgrounds, - limit - ) - - return divTable:create() + local pointsTable = AutomaticPointsTable(frame):process() + return pointsTable:display() end -function AutomaticPointsTable:storeLPDB(pointsData) - local date = os.date() - Array.forEach(pointsData, function(teamPointsData) - local team = teamPointsData.team - local teamName = string.lower(team.aliases[#team.aliases]) +---@param opponents AutomaticPointsTableOpponent[] +function AutomaticPointsTable:storeLPDB(opponents) + local date = DateExt.getContextualDateOrNow() + Array.forEach(opponents, function(opponent) + local opponentName = Opponent.toName(opponent.opponent) local lpdbName = self.parsedInput.lpdbName - local uniqueId = teamName .. '_' .. lpdbName - local position = teamPointsData.position - local totalPoints = teamPointsData.totalPoints + local uniqueId = opponentName .. '_' .. lpdbName + local position = opponent.placement + local totalPoints = opponent.totalPoints local objectData = { type = 'automatic_points', - name = teamName, + name = opponentName, information = position, date = date, - extradata = Json.stringify({ + extradata = { position = position, totalPoints = totalPoints - }) + } } - mw.ext.LiquipediaDB.lpdb_datapoint(uniqueId, objectData) + mw.ext.LiquipediaDB.lpdb_datapoint(uniqueId, Json.stringifySubTables(objectData)) end) end +---@param args table +---@return AutomaticPointsTableConfig function AutomaticPointsTable:parseInput(args) local positionBackgrounds = self:parsePositionBackgroundData(args) local tournaments = self:parseTournaments(args) - local shouldResolveRedirect = Logic.readBool(args.resolveRedirect) - local teams = self:parseTeams(args, #tournaments, shouldResolveRedirect) + local opponents = self:parseOpponents(args, tournaments) local minified = Logic.readBool(args.minified) - local limit = tonumber(args.limit) or #teams + local limit = tonumber(args.limit) or #opponents local lpdbName = args.lpdbName or mw.title.getCurrentTitle().text return { positionBackgrounds = positionBackgrounds, tournaments = tournaments, - teams = teams, + opponents = opponents, shouldTableBeMinified = minified, limit = limit, lpdbName = lpdbName, - shouldResolveRedirect = shouldResolveRedirect, } end --- parses the positionbg arguments, these are the background colors of specific --- positions, usually used to indicate if a team in a specific position will end up qualifying +---@param args table +---@return string[] function AutomaticPointsTable:parsePositionBackgroundData(args) local positionBackgrounds = {} for _, background in Table.iter.pairsByPrefix(args, 'positionbg') do @@ -125,45 +132,84 @@ function AutomaticPointsTable:parsePositionBackgroundData(args) return positionBackgrounds end +---@param args table +---@return StandardTournament[] function AutomaticPointsTable:parseTournaments(args) local tournaments = {} for _, tournament in Table.iter.pairsByPrefix(args, 'tournament') do - table.insert(tournaments, (Json.parse(tournament))) + Array.appendWith(tournaments, Tournament.getTournament(tournament)) end return tournaments end -function AutomaticPointsTable:parseTeams(args, tournamentCount, shouldResolveRedirect) - local teams = {} - for _, team in Table.iter.pairsByPrefix(args, 'team') do - local parsedTeam = Json.parse(team) - parsedTeam.aliases = self:parseAliases(parsedTeam, tournamentCount, shouldResolveRedirect) - parsedTeam.deductions = self:parseDeductions(parsedTeam, tournamentCount) - parsedTeam.manualPoints = self:parseManualPoints(parsedTeam, tournamentCount) - parsedTeam.tiebreakerPoints = tonumber(parsedTeam.tiebreaker_points) or 0 - parsedTeam.results = {} - table.insert(teams, parsedTeam) +---@param args table +---@param tournaments StandardTournament[] +---@return AutomaticPointsTableOpponent[] +function AutomaticPointsTable:parseOpponents(args, tournaments) + local opponents = {} + for _, opponentArgs in Table.iter.pairsByPrefix(args, 'opponent') do + local parsedArgs = Json.parseIfString(opponentArgs) + local parsedOpponent = { + opponent = Opponent.readOpponentArgs(parsedArgs), + tiebreakerPoints = tonumber(parsedArgs.tiebreaker) or 0, + background = parsedArgs.bg, + note = parsedArgs.note, + } + assert(parsedOpponent.opponent.type == Opponent.team) + assert(not Opponent.isTbd(parsedOpponent.opponent)) + local aliases = self:parseAliases(parsedArgs, parsedOpponent.opponent, #tournaments) + + parsedOpponent.results = Array.map(tournaments, function (tournament, tournamentIndex) + local manualPoints = parsedArgs['points' .. tournamentIndex] + -- TODO: Automate qualified once #6762 is merged + local qualified = Logic.readBoolOrNil(parsedArgs['qualified' .. tournamentIndex]) + if String.isNotEmpty(manualPoints) then + return Table.mergeInto({ + qualified = qualified, + type = POINTS_TYPE.MANUAL, + amount = tonumber(manualPoints) + + }, self:parseDeduction(parsedArgs, tournament, tournamentIndex)) + end + + local queriedPoints = self:queryPlacement(aliases[tournamentIndex], tournament) + + if not queriedPoints then + return {} + end + + return Table.merge( + queriedPoints, {qualified = qualified}, self:parseDeduction(parsedArgs, tournament, tournamentIndex) + ) + end) + + parsedOpponent.totalPoints = Array.reduce(parsedOpponent.results, function (aggregate, result) + return aggregate + (result.amount or 0) - (result.deduction or 0) + end, 0) + + parsedOpponent.qualified = Array.any(parsedOpponent.results, Operator.property('qualified')) + + Array.appendWith(opponents, parsedOpponent) end - return teams + return opponents end --- Parses the team aliases, used in cases where a team is picked up by an org or changed --- name in some of the tournaments, in which case aliases are required to correctly query --- the team's results & points -function AutomaticPointsTable:parseAliases(team, tournamentCount, shouldResolveRedirect) +---@param args table +---@param opponent standardOpponent +---@param tournamentCount integer +---@return string[][] +function AutomaticPointsTable:parseAliases(args, opponent, tournamentCount) local aliases = {} - local parseAlias = function(x) - if (shouldResolveRedirect) then - return mw.ext.TeamLiquidIntegration.resolve_redirect(x) - end - return mw.language.getContentLanguage():ucfirst(x) - end - local lastAlias = team.name + local lastAliases = TeamTemplate.queryHistoricalNames(Opponent.toName(opponent)) + for index = 1, tournamentCount do - if String.isNotEmpty(team['alias' .. index]) then - lastAlias = team['alias' .. index] + if String.isNotEmpty(args['alias' .. index]) then + lastAliases = TeamTemplate.queryHistoricalNames(args['alias' .. index]) end - aliases[index] = parseAlias(lastAlias) + aliases[index] = lastAliases end return aliases end @@ -171,134 +217,61 @@ end --- Parses the teams' deductions, used in cases where a team has disbanded or made a roster --- change that causes them to lose a portion or all of their points that they've accumulated --- up until that change -function AutomaticPointsTable:parseDeductions(team, tournamentCount) - local deductions = {} - for index = 1, tournamentCount do - if String.isNotEmpty(team['deduction' .. index]) then - if not deductions[index] then - deductions[index] = {} - end - deductions[index].amount = tonumber(team['deduction' .. index]) - - if String.isNotEmpty(team['deduction' .. index .. 'note']) then - deductions[index].note = team['deduction' .. index .. 'note'] - end - end - end - - return deductions -end - -function AutomaticPointsTable:parseManualPoints(team, tournamentCount) - local manualPoints = {} - for index = 1, tournamentCount do - if String.isNotEmpty(team['points' .. index]) then - manualPoints[index] = tonumber(team['points' .. index]) - end +---@param args table +---@param tournament StandardTournament +---@param tournamentIndex integer +---@return {deduction: number?, note: string?}[] +function AutomaticPointsTable:parseDeduction(args, tournament, tournamentIndex) + local deduction = args['deduction' .. tournamentIndex] + if String.isEmpty(deduction) then + return {} + elseif not Logic.isNumeric(deduction) then + return {} end - return manualPoints -end - -function AutomaticPointsTable:generateReverseAliases(teams, tournaments) - local reverseAliases = {} - local shouldResolveRedirect = self.parsedInput.shouldResolveRedirect - for tournamentIndex = 1, #tournaments do - reverseAliases[tournamentIndex] = {} - Array.forEach(teams, - function(team, index) - local alias - if shouldResolveRedirect then - alias = mw.ext.TeamLiquidIntegration.resolve_redirect(team.aliases[tournamentIndex]) - else - alias = team.aliases[tournamentIndex] - end - reverseAliases[tournamentIndex][alias] = index - end - ) - end - return reverseAliases -end - - -function AutomaticPointsTable:queryPlacements(teams, tournaments) - -- to get a team index, use reverseAliases[tournamentIndex][alias] - local reverseAliases = self:generateReverseAliases(teams, tournaments) - - local queryParams = { - limit = 5000, - query = 'tournament, participant, placement, extradata' + tournament.extradata.includesDeduction = true + return { + deduction = tonumber(deduction), + note = args['deduction' .. tournamentIndex .. 'note'] } - - local tree = ConditionTree(BooleanOperator.any) - local columnName = ColumnName('tournament') - local tournamentIndices = {} - Array.forEach(tournaments, - function(t, index) - tree:add(ConditionNode(columnName, Comparator.eq, t.name)) - tournamentIndices[t.name] = index - t.placements = {} - end - ) - local conditions = tree:toString() - - queryParams.conditions = conditions - local allQueryResult = mw.ext.LiquipediaDB.lpdb('placement', queryParams) - - Array.forEach(allQueryResult, - function(result) - local tournamentIndex = tournamentIndices[result.tournament] - local tournament = tournaments[tournamentIndex] - - result.prizePoints = tonumber(result.extradata.prizepoints) - result.securedPoints = tonumber(result.extradata.securedpoints) - result.extradata = nil - table.insert(tournament.placements, result) - - local participant = result.participant - local teamIndex = reverseAliases[tournamentIndex][participant] - if teamIndex ~= nil then - teams[teamIndex].results[tournamentIndex] = result - end - end - ) - - return teams, tournaments end -function AutomaticPointsTable:getPointsData(teams, tournaments) - return Table.mapValues(teams, - function(team) - local teamPointsData = {} - local totalPoints = 0 - for tournamentIndex = 1, #tournaments do - local manualPoints = team.manualPoints[tournamentIndex] - local placement = team.results[tournamentIndex] - - local pointsForTournament = self:calculatePointsForTournament(placement, manualPoints) - if Table.isNotEmpty(pointsForTournament) then - totalPoints = totalPoints + pointsForTournament.amount - end +---@param aliases string[] +---@param tournament StandardTournament +function AutomaticPointsTable:queryPlacement(aliases, tournament) + local conditions = ConditionTree(BooleanOperator.all):add{ + ConditionNode(ColumnName('parent'), Comparator.eq, tournament.pageName), + ConditionNode(ColumnName('opponenttype'), Comparator.eq, Opponent.team), + Condition.Util.anyOf(ColumnName('opponenttemplate'), aliases), + } + local result = mw.ext.LiquipediaDB.lpdb('placement', { + conditions = tostring(conditions), + limit = 1, + query = 'extradata' + })[1] + + if not result then + return + end - local deduction = team.deductions[tournamentIndex] - if Table.isNotEmpty(deduction) then - pointsForTournament.deduction = deduction - -- will only show the deductions column if there's atleast one team with - -- some deduction for a tournament - tournaments[tournamentIndex].shouldDeductionsBeVisible = true - totalPoints = totalPoints - (deduction.amount or 0) - end + local prizePoints = tonumber(result.extradata.prizepoints) + local securedPoints = tonumber(result.extradata.securedpoints) - teamPointsData[tournamentIndex] = pointsForTournament - end - - teamPointsData.team = team - teamPointsData.totalPoints = totalPoints - teamPointsData.tiebreakerPoints = team.tiebreakerPoints - return teamPointsData - end - ) + if prizePoints then + return { + amount = prizePoints, + type = POINTS_TYPE.PRIZE, + } + elseif securedPoints then + return { + amount = securedPoints, + type = POINTS_TYPE.SECURED, + } + end end +---@param placement {prizePoints: number?, securedPoints: number?} +---@param manualPoints number? +---@return {amount: number?, type: PointsType?} function AutomaticPointsTable:calculatePointsForTournament(placement, manualPoints) -- manual points get highest priority if manualPoints ~= nil then @@ -329,44 +302,68 @@ function AutomaticPointsTable:calculatePointsForTournament(placement, manualPoin end --- sort by total points (desc) then by name (asc) -function AutomaticPointsTable:sortData(pointsData, teams) - table.sort(pointsData, - function(a, b) - if a.totalPoints ~= b.totalPoints then - return a.totalPoints > b.totalPoints +---@param opponents AutomaticPointsTableOpponent[] +---@return AutomaticPointsTableOpponent[] +function AutomaticPointsTable:sortData(opponents) + return Array.sortBy(opponents, FnUtil.identity, + ---@param opp1 AutomaticPointsTableOpponent + ---@param opp2 AutomaticPointsTableOpponent + function (opp1, opp2) + if opp1.qualified then + if not opp2.qualified then + return true + end + elseif opp2.qualified then + return false end - if a.tiebreakerPoints ~= b.tiebreakerPoints then - return a.tiebreakerPoints > b.tiebreakerPoints + local totalPoints1 = opp1.totalPoints + local totalPoints2 = opp2.totalPoints + if totalPoints1 ~= totalPoints2 then + return totalPoints1 > totalPoints2 + elseif opp1.tiebreakerPoints ~= opp2.tiebreakerPoints then + return opp1.tiebreakerPoints > opp2.tiebreakerPoints end - local aName = a.team.aliases[#a.team.aliases] - local bName = b.team.aliases[#b.team.aliases] - return aName < bName + return Opponent.toName(opp1.opponent) < Opponent.toName(opp2.opponent) end ) +end - return pointsData +---@param opponents AutomaticPointsTableOpponent[] +---@return AutomaticPointsTableOpponent[] +function AutomaticPointsTable:addPlacements(opponents) + Array.forEach(opponents, function (opponent, index) + if index == 1 then + opponent.placement = index + elseif opponent.qualified ~= opponents[index - 1].qualified then + opponent.placement = index + elseif opponent.totalPoints ~= opponents[index - 1].totalPoints then + opponent.placement = index + elseif opponent.tiebreakerPoints ~= opponents[index - 1].tiebreakerPoints then + opponent.placement = index + else + opponent.placement = opponents[index - 1].placement + end + end) + return opponents end -function AutomaticPointsTable:addPositionData(pointsData) - local teamPosition = 0 - local previousTotalPoints = pointsData[1].totalPoints + 1 - local previousTiebreakerPoints = pointsData[1].tiebreakerPoints + 1 - - return Table.map(pointsData, - function(index, dataPoint) - local lessTotalPoints = dataPoint.totalPoints < previousTotalPoints - local equalTotalPoints = dataPoint.totalPoints == previousTotalPoints - local lessTiebreakerPoints = dataPoint.tiebreakerPoints < previousTiebreakerPoints - if lessTotalPoints or (equalTotalPoints and lessTiebreakerPoints) then - teamPosition = index - end - dataPoint.position = teamPosition - previousTotalPoints = dataPoint.totalPoints - previousTiebreakerPoints = dataPoint.tiebreakerPoints +---@return self +function AutomaticPointsTable:process() + self.opponents = self:addPlacements(self:sortData(self.parsedInput.opponents)) + if Lpdb.isStorageEnabled() then + self:storeLPDB(self.opponents) + end + return self +end - return index, dataPoint - end - ) +---@return Widget +function AutomaticPointsTable:display() + return AutomaticPointsTableWidget{ + opponents = self.opponents, + tournaments = self.parsedInput.tournaments, + limit = self.parsedInput.limit, + positionBackgrounds = self.parsedInput.positionBackgrounds, + } end return AutomaticPointsTable diff --git a/lua/wikis/commons/AutomaticPointsTable/Display.lua b/lua/wikis/commons/AutomaticPointsTable/Display.lua deleted file mode 100644 index 59e1600ffd1..00000000000 --- a/lua/wikis/commons/AutomaticPointsTable/Display.lua +++ /dev/null @@ -1,248 +0,0 @@ ---- --- @Liquipedia --- page=Module:AutomaticPointsTable/Display --- --- 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 Logic = Lua.import('Module:Logic') -local String = Lua.import('Module:StringUtils') -local Table = Lua.import('Module:Table') - -local POINTS_TYPE = { - MANUAL = 'MANUAL', - PRIZE = 'PRIZE', - SECURED = 'SECURED' -} - -local PointsDivTable = Class.new( - function(self, pointsData, tournaments, positionBackgrounds, limit) - self.root = mw.html.create('div') :addClass('divTable') - :addClass('border-color-grey') :addClass('border-bottom') - - local columnCount = Array.reduce(tournaments, function(count, t) - return count + (t.shouldDeductionsBeVisible and 2 or 1) - end, 0) - - local innerWrapper = mw.html.create('div') :addClass('fixed-size-table-container') - :addClass('border-color-grey') - :css('width', tostring(450 + (columnCount * 50)) .. 'px') - :node(self.root) - - self.wrapper = mw.html.create('div') :addClass('table-responsive') - :addClass('automatic-points-table') - :node(innerWrapper) - - self.rows = {} - self.pointsData = pointsData - self.tournaments = tournaments - self.positionBackgrounds = positionBackgrounds - self.limit = limit - end -) - -local TableRow = Class.new( - function(self, pointsData, tournaments, positionBackground) - self.root = mw.html.create('div'):addClass('divRow') - self.cells = {} - - local team = pointsData.team - if team.bg then - self.root:addClass('bg-' .. team.bg) - end - - self.pointsData = pointsData - self.tournaments = tournaments - self.team = team - self.positionBackground = positionBackground - end -) - -local TableHeaderRow = Class.new( - function(self, tournaments) - self.tournaments = tournaments - self.root = mw.html.create('div') :addClass('divHeaderRow') :addClass('diagonal') - self.cells = {} - end -) - -function PointsDivTable:row(row) - table.insert(self.rows, row) - return self -end - -function PointsDivTable:create() - local headerRow = TableHeaderRow(self.tournaments) - self:row(headerRow) - - local limit = self.limit - Array.forEach(self.pointsData, function(teamPointsData, index) - if index > limit then return end - local positionBackground = self.positionBackgrounds[index] - self:row(TableRow(teamPointsData, self.tournaments, positionBackground)) - end) - - for _, row in pairs(self.rows) do - self.root:node(row:create()) - end - - return self.wrapper -end - -function TableRow:create() - -- fixed cells - self:positionCell(self.pointsData.position, self.positionBackground) - self:nameCell(self.team) - self:totalCell(self.pointsData.totalPoints) - -- variable cells - Array.forEach(self.pointsData, function(points, index) - local tournament = self.tournaments[index] - self:pointsCell(points, tournament) - if tournament.shouldDeductionsBeVisible then - self:deductionCell(points.deduction) - end - end) - - for _, cell in pairs(self.cells) do - self.root:node(cell) - end - return self.root -end - -local function wrapInDiv(text) - return mw.html.create('div'):wikitext(tostring(text)) -end - -function TableRow:baseCell(text, bg, bold) - local div = wrapInDiv(text) :addClass('divCell') :addClass('va-middle') - :addClass('centered-cell') :addClass('border-color-grey') :addClass('border-top-right') - - if bg then - div:addClass('bg-' .. bg) - end - if bold then - div:css('font-weight', 'bold') - end - return div -end - -function TableRow:totalCell(points) - local totalCell = self:baseCell(points, nil, true) - table.insert(self.cells, totalCell) - return self -end - -function TableRow:positionCell(position, bg) - local positionCell = self:baseCell(position .. '.', bg, true) - table.insert(self.cells, positionCell) - return self -end - -function TableRow:nameCell(team) - local lastAlias = team.aliases[#team.aliases] - local teamDisplay = team.display and team.display or mw.ext.TeamTemplate.team(lastAlias) - local nameCell = self:baseCell(teamDisplay, team.bg):addClass('name-cell') - table.insert(self.cells, nameCell) - return self -end - -function TableRow:pointsCell(points, tournament) - local finished = Logic.readBool(tournament.finished) - local pointString = points.amount ~= nil and points.amount or (finished and '-' or '') - local pointsCell = self:baseCell(pointString) - if points.type == POINTS_TYPE.SECURED then - pointsCell:css('font-weight', 'lighter'):css('font-style', 'italic') - end - table.insert(self.cells, pointsCell) - return self -end - -function TableRow:deductionCell(deduction) - if Table.isEmpty(deduction) then - table.insert(self.cells, self:baseCell('')) - return self - end - local abbr = mw.html.create('abbr'):addClass('bg-down'):addClass('deduction-box') - :wikitext(deduction.amount) - if String.isNotEmpty(deduction.note) then - abbr:attr('title', deduction.note) - end - local deductionCell = self:baseCell(abbr) - table.insert(self.cells, deductionCell) - return self -end - -function TableHeaderRow:cell(header) - local additionalClass = header.additionalClass - - local innerDiv = wrapInDiv(header.text) :addClass('border-color-grey') - :addClass('content') - - local outerDiv = mw.html.create('div') :addClass('divCell') - :addClass('diagonal-header-div-cell') - if additionalClass then - outerDiv:addClass(additionalClass) - end - outerDiv:node(innerDiv) - table.insert(self.cells, outerDiv) - return self -end - -function TableHeaderRow:create() - -- fixed headers - local headers = {{ - text = 'Ranking', - additionalClass = 'ranking' - }, { - text = 'Team', - additionalClass = 'team' - }, { - text = 'Total Points' - }} - Array.forEach(headers, function(h) self:headerCell(h) end) - -- variable headers (according to tournaments in given in module arguments) - Array.forEach(self.tournaments, - function(tournament) - self:headerCell({ - text = tournament.display and tournament.display or tournament.name - }) - - if tournament.shouldDeductionsBeVisible then - local deductionsHeader = tournament['deductionsheader'] - self:headerCell({ - text = deductionsHeader and deductionsHeader or 'Deductions' - }) - end - end - ) - - for _, cell in pairs(self.cells) do - self.root:node(cell) - end - return self.root -end - -function TableHeaderRow:headerCell(header) - local innerDiv = wrapInDiv(header.text) - :addClass('border-color-grey') - :addClass('content') - - local outerDiv = mw.html.create('div') :addClass('divCell') - :addClass('diagonal-header-div-cell') - - local additionalClass = header.additionalClass - if additionalClass then - - outerDiv:addClass(additionalClass) - end - outerDiv:node(innerDiv) - - table.insert(self.cells, outerDiv) - return self -end - -return PointsDivTable diff --git a/lua/wikis/commons/AutomaticPointsTable/MinifiedDisplay.lua b/lua/wikis/commons/AutomaticPointsTable/MinifiedDisplay.lua deleted file mode 100644 index 37168822776..00000000000 --- a/lua/wikis/commons/AutomaticPointsTable/MinifiedDisplay.lua +++ /dev/null @@ -1,182 +0,0 @@ ---- --- @Liquipedia --- page=Module:AutomaticPointsTable/MinifiedDisplay --- --- 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 PointsDivTable = Class.new( - function(self, pointsData, tournaments, positionBackgrounds, limit) - self.root = mw.html.create('div') :addClass('divTable') - :addClass('border-color-grey') :addClass('border-bottom') - - local innerWrapper = mw.html.create('div') :addClass('fixed-size-table-container') - :addClass('border-color-grey') - :css('width', '316px') - :node(self.root) - - self.wrapper = mw.html.create('div') :addClass('table-responsive') - :addClass('automatic-points-table') :addClass('minified') - :node(innerWrapper) - - self.rows = {} - self.pointsData = pointsData - self.tournaments = tournaments - self.positionBackgrounds = positionBackgrounds - self.limit = limit - end -) - -local TableRow = Class.new( - function(self, pointsData, tournaments, positionBackground) - self.root = mw.html.create('div'):addClass('divRow') - self.cells = {} - - local team = pointsData.team - if team.bg then - self.root:addClass('bg-' .. team.bg) - end - - self.pointsData = pointsData - self.tournaments = tournaments - self.team = team - self.positionBackground = positionBackground - end -) - -local TableHeaderRow = Class.new( - function(self, tournaments) - self.tournaments = tournaments - self.root = mw.html.create('div') :addClass('divHeaderRow') - self.cells = {} - end -) - -function PointsDivTable:row(row) - table.insert(self.rows, row) - return self -end - -function PointsDivTable:create() - local headerRow = TableHeaderRow(self.tournaments) - self:row(headerRow) - - local limit = self.limit - Array.forEach(self.pointsData, function(teamPointsData, index) - if index > limit then return end - local positionBackground = self.positionBackgrounds[index] - self:row(TableRow(teamPointsData, self.tournaments, positionBackground)) - end) - - for _, row in pairs(self.rows) do - self.root:node(row:create()) - end - - return self.wrapper -end - -function TableRow:create() - -- fixed cells - self:positionCell(self.pointsData.position, self.positionBackground) - self:nameCell(self.team) - self:totalCell(self.pointsData.totalPoints) - - for _, cell in pairs(self.cells) do - self.root:node(cell) - end - return self.root -end - -local function wrapInDiv(text) - return mw.html.create('div'):wikitext(tostring(text)) -end - -function TableRow:baseCell(text, bg, bold) - local div = wrapInDiv(text) :addClass('divCell') :addClass('va-middle') - :addClass('centered-cell') :addClass('border-color-grey') :addClass('border-top-right') - - if bg then - div:addClass('bg-' .. bg) - end - if bold then - div:css('font-weight', 'bold') - end - return div -end - -function TableRow:totalCell(points) - local totalCell = self:baseCell(points, nil, true) - table.insert(self.cells, totalCell) - return self -end - -function TableRow:positionCell(position, bg) - local positionCell = self:baseCell(position .. '.', bg, true) - table.insert(self.cells, positionCell) - return self -end - -function TableRow:nameCell(team) - local lastAlias = team.aliases[#team.aliases] - local teamDisplay = team.display and team.display or mw.ext.TeamTemplate.team(lastAlias) - local nameCell = self:baseCell(teamDisplay, team.bg):addClass('name-cell') - table.insert(self.cells, nameCell) - return self -end - -function TableHeaderRow:cell(header) - local additionalClass = header.additionalClass - - local innerDiv = wrapInDiv(header.text) :addClass('border-color-grey') - :addClass('content') - - local outerDiv = mw.html.create('div') :addClass('divCell') - if additionalClass then - outerDiv:addClass(additionalClass) - end - outerDiv:node(innerDiv) - table.insert(self.cells, outerDiv) - return self -end - -function TableHeaderRow:create() - -- fixed headers - local headers = {{ - text = '#', - width = '35px' - }, { - text = 'Team', - width = '225px' - }, { - text = 'Points', - width = '50px' - }} - Array.forEach(headers, function(h) self:headerCell(h) end) - - for _, cell in pairs(self.cells) do - self.root:node(cell) - end - return self.root -end - -function TableHeaderRow:headerCell(header) - local innerDiv = wrapInDiv(header.text) - :addClass('border-color-grey') - :addClass('content') - - local outerDiv = mw.html.create('div') :addClass('divCell') - :addClass('border-right') :css('text-align', 'center') - - outerDiv:css('width', header.width) - outerDiv:node(innerDiv) - - table.insert(self.cells, outerDiv) - return self -end - -return PointsDivTable diff --git a/lua/wikis/commons/Widget/AutomaticPointsTable.lua b/lua/wikis/commons/Widget/AutomaticPointsTable.lua new file mode 100644 index 00000000000..75c8b591868 --- /dev/null +++ b/lua/wikis/commons/Widget/AutomaticPointsTable.lua @@ -0,0 +1,185 @@ +--- +-- @Liquipedia +-- page=Module:Widget/AutomaticPointsTable +-- +-- 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 OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') + +local Widget = Lua.import('Module:Widget') +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local Div = HtmlWidgets.Div +local IconFa = Lua.import('Module:Widget/Image/Icon/Fontawesome') +local TournamentTitle = Lua.import('Module:Widget/Tournament/Title') +local WidgetUtil = Lua.import('Module:Widget/Util') + +local QUALIFIED_ICON = IconFa{iconName = 'qualified', color = 'forest-green-text', hover = 'Qualified'} + +---@class AutomaticPointsTableWidgetProps +---@field opponents AutomaticPointsTableOpponent +---@field tournaments StandardTournament[] +---@field limit integer +---@field positionBackgrounds string[] + +---@class AutomaticPointsTableWidget: Widget +---@operator call(AutomaticPointsTableWidgetProps): AutomaticPointsTableWidget +---@field props AutomaticPointsTableWidgetProps +local AutomaticPointsTableWidget = Class.new(Widget) + +---@return Widget +function AutomaticPointsTableWidget:render() + local numCols = Array.reduce(self.props.tournaments, function (aggregate, tournament) + if tournament.extradata.includesDeduction then + return aggregate + 2 + end + return aggregate + 1 + end, 0) + return Div{ + classes = {'table-responsive', 'automatic-points-table'}, + children = Div{ + classes = {'fixed-size-table-container', 'border-color-grey'}, + css = {width = (450 + numCols * 50) .. 'px'}, + children = self:createTable() + } + } +end + +---@protected +---@return Widget +function AutomaticPointsTableWidget:createTable() + return Div{ + classes = {'divTable', 'border-color-grey', 'border-bottom'}, + children = WidgetUtil.collect( + self:createHeader(), + Array.map(self.props.opponents, function (opponent, opponentIndex) + return self:createRow(opponent, opponentIndex) + end) + ) + } +end + +---@private +---@param child string|Widget +---@param additionalClass string? +---@return Widget +function AutomaticPointsTableWidget._createHeaderCell(child, additionalClass) + return Div{ + classes = {'divCell', 'diagonal-header-div-cell', additionalClass}, + children = Div{ + classes = {'border-color-grey', 'content'}, + children = child + } + } +end + +---@protected +---@return Widget +function AutomaticPointsTableWidget:createHeader() + local tournaments = self.props.tournaments + + return Div{ + classes = {'divHeaderRow', 'diagonal'}, + children = WidgetUtil.collect( + AutomaticPointsTableWidget._createHeaderCell('Ranking','ranking'), + AutomaticPointsTableWidget._createHeaderCell('Team', 'team'), + AutomaticPointsTableWidget._createHeaderCell('Total Points'), + Array.flatMap(tournaments, function (tournament) + return { + AutomaticPointsTableWidget._createHeaderCell(TournamentTitle{tournament = tournament}), + tournament.extradata.includesDeduction and AutomaticPointsTableWidget._createHeaderCell('Deductions') or nil + } + end) + ) + } +end + +---@private +---@param props {children: string|number|Widget|Html|(string|number|Widget|Html)[]?, +---additionalClasses: string[]?, background: string?, bold: boolean?, css: table?} +---@return Widget +function AutomaticPointsTableWidget._createRowCell(props) + return Div{ + classes = Array.extend( + 'divCell', + 'va-middle', + 'centered-cell', + 'border-color-grey', + 'border-top-right', + props.background and ('bg-' .. props.background) or nil, + props.additionalClasses + ), + css = props.css, + children = Div{ + classes = {'border-color-grey', 'content'}, + children = props.children + } + } +end + +---@protected +---@param opponent AutomaticPointsTableOpponent +---@param opponentIndex integer +---@return Widget +function AutomaticPointsTableWidget:createRow(opponent, opponentIndex) + return Div{ + classes = {'divRow', opponent.background and ('bg-' .. opponent.background) or nil}, + children = WidgetUtil.collect( + AutomaticPointsTableWidget._createRowCell{ + background = self.props.positionBackgrounds[opponentIndex], + children = opponent.placement, + css = {['font-weight'] = 'bold'}, + }, + AutomaticPointsTableWidget._createRowCell{ + additionalClasses = {'name-cell'}, + background = opponent.background, + children = OpponentDisplay.BlockOpponent{opponent = opponent.opponent, note = opponent.note}, + }, + self:_createTotalPointsCell(opponent), + Array.flatMap(opponent.results, function (result, resultIndex) + local resultDisplay + if result.qualified then + resultDisplay = QUALIFIED_ICON + elseif result.amount then + resultDisplay = result.amount + elseif self.props.tournaments[resultIndex].phase == 'FINISHED' then + resultDisplay = '-' + end + return { + AutomaticPointsTableWidget._createRowCell{ + css = result.type == 'SECURED' and { + ['font-weight'] = 'lighter', + ['font-style'] = 'italic' + } or nil, + children = resultDisplay, + }, + self.props.tournaments[resultIndex].extradata.includesDeduction and AutomaticPointsTableWidget._createRowCell{ + children = result.deduction and { + HtmlWidgets.Abbr{ + classes = {'deduction-box'}, + title = result.note, + children = result.deduction + } + } or nil + } or nil + } + end) + ) + } +end + +---@private +---@param opponent AutomaticPointsTableOpponent +---@return Widget +function AutomaticPointsTableWidget:_createTotalPointsCell(opponent) + return AutomaticPointsTableWidget._createRowCell{ + css = {['font-weight'] = 'bold'}, + children = opponent.qualified and QUALIFIED_ICON or opponent.totalPoints, + } +end + +return AutomaticPointsTableWidget diff --git a/stylesheets/commons/AutomaticPointsTable.scss b/stylesheets/commons/AutomaticPointsTable.scss index 87f7f9adb18..f4c811e2372 100644 --- a/stylesheets/commons/AutomaticPointsTable.scss +++ b/stylesheets/commons/AutomaticPointsTable.scss @@ -40,7 +40,7 @@ Author(s): XtratoS white-space: nowrap; margin-left: -5px; padding-top: 6px; - background-color: #f5f5f5; + background-color: var( --table-header-variant-background-color ); } .automatic-points-table .divCell.border-top-right {