Module:TeamHistoryAuto

From Liquipedia Commons Wiki
Module documentation[view] [edit] [history] [purge]

This module is used to generate a team history table within {{Infobox player}}. It utilises transfer data saved to LPDB via {{Transfer Row}}.
While every matching transfer is listed in the LUA logs, only those with a joindate are eventually displayed.

Parameters[edit]

|player=
Player page name (default: subpage name of the current page)
|returnEmptyIfNoResults=
(Optional) If no transfers are found the module will return an empty string/nil instead of an empty (not displayed) html table
|hiderole=
(Optional) Disables the role display
|addlpdbdata=
(Optional) Enables the team history storage in lpdb data points
|specialRoles=
(Optional) If enabled transfers with empty toteam but special role as role2 (Retired, Military) are also considered valid
|cleanRoles=
(Optional) Cleanes roles from invalid ones via a provided module.
|convertrole=
(Optional) Converts transfer roles (via Module:TeamHistoryAuto/role)
|iconModule=
(Optional) Adds icons to transfers based on the content in linked module. If the wiki is using positions, this is usually Module:PositionIcon/data.

Examples[edit]

See also[edit]


--remaining issue: player joins/leaves same team with multiple roles on different dates (e.g. MarioMe LoL wiki)

--[[things still not meeting code style for the git:
- several spans etc created as strings
- condition building could use some further cleanup
- move remaining const stuff as const vars up top
- make it a class so we can kick `_config`
]]

local Array = require('Module:Array')
local Class = require('Module:Class')
local DateExt = require('Module:Date/Ext')
local Logic = require('Module:Logic')
local String = require('Module:StringUtils')
local Table = require('Module:Table')
local Team = require('Module:Team')

local LANG = mw.language.getContentLanguage()
local SPECIAL_ROLES = {'Retired', 'Retirement', 'Military'}
local SPECIAL_ROLES_LOWER = Array.map(SPECIAL_ROLES, string.lower)
local LOAN = 'Loan'
local ONE_DAY = 86400

local _config

local TeamHistoryAuto = {}

function TeamHistoryAuto._results(args)
	return TeamHistoryAuto.results(args)
end

function TeamHistoryAuto.results(args)
	args = args or {}

	TeamHistoryAuto._readConfig(args)

	local transferList = TeamHistoryAuto._buildTransferList()

	-- imo we should make this the default in the future, but would need some adjusts in player infoboxes when checking for an empty one
	if _config.returnEmptyIfNoResults and Table.isEmpty(transferList) then
		return
	end

	local display = mw.html.create('table')
		:css('width', '100%')
		:css('text-align', 'left')

	for teamCount, transfer in ipairs(transferList) do
		display:node(TeamHistoryAuto._displayTransfer(transfer, teamCount))
	end

	return tostring(display)
end

function TeamHistoryAuto._readConfig(args)
	_config = {
		showRole = not Logic.readBool(args.hiderole),
		player = (args.player or mw.title.getCurrentTitle().subpageText):gsub('^%l', string.upper),
		store = Logic.readBool(args.addlpdbdata),
		specialRoles = Logic.readBool(args.specialRoles),
		roleConvert = Logic.readBool(args.convertrole) and mw.loadData('Module:TeamHistoryAuto/role'),
		roleClean = args.cleanRoles and mw.loadData(args.cleanRoles),
		iconConvert = args.iconModule and mw.loadData(args.iconModule),
		returnEmptyIfNoResults = Logic.readBool(Logic.emptyOr(args.returnEmptyIfNoResults, true)),
	}
end

function TeamHistoryAuto._buildTransferList()
	local roleConditions = _config.specialRoles and (' OR ' .. table.concat(Array.map(SPECIAL_ROLES, function(role)
		return '[[role2::' .. role .. ']] OR [[role2::' .. role:lower() .. ']]'
	end), ' OR ')) or ''

	local transferData = mw.ext.LiquipediaDB.lpdb('transfer', {
		conditions = '[[player::' .. _config.player .. ']] AND ([[toteam::>]]' .. roleConditions .. ') AND [[date::>' .. DateExt.defaultDate .. ']]',
		order = 'date asc',
		limit = 5000,
		query = 'pagename, fromteam, toteam, role1, role2, date, extradata'
	}) --[[@as transfer[]?]]

	local transferList = {}

	-- Process transfer (team, role, join date)
	for _, transfer in ipairs(transferData or {}) do
		-- need it like this; can not insert directly, else we get errors in some edge cases
		local transferEntry = TeamHistoryAuto._processTransfer(transfer, transferList)
	end

	-- release transferData to free up memory
	transferData = nil

	for _, transfer in pairs(transferList) do
		if _config.roleClean then
			transfer.role = _config.roleClean[(transfer.role or ''):lower()]
		end
		TeamHistoryAuto._completeTransfer(transfer)
	end

	-- Sort table by joinDate/leaveDate
	table.sort(transferList, function(transfer1, transfer2)
		if transfer1.joinDate == transfer2.joinDate then
			if transfer1.role == LOAN and transfer2.role ~= LOAN then
				return false
			elseif transfer2.role == LOAN and transfer1.role ~=LOAN then
				return true
			end

			return (transfer1.leaveDate or '') < (transfer2.leaveDate or '')
		end

		return transfer1.joinDate < transfer2.joinDate
	end)

	return transferList
end

function TeamHistoryAuto._processTransfer(transfer, transferList)
	local extraData = transfer.extradata
	local transferDate = LANG:formatDate('Y-m-d', transfer.date)

	if String.isNotEmpty(extraData.toteamsec) then
		-- transfer includes multiple teams (Tl:Transfer_row |team2_2, |role2_2)
		if (extraData.toteamsec ~= transfer.fromteam or extraData.role2sec ~= transfer.role1) and
			(extraData.toteamsec ~= extraData.fromteamsec or extraData.role2sec ~= extraData.role1sec) then
			-- secondary transfer
			table.insert(transferList, {
				team = extraData.toteamsec,
				role = extraData.role2sec,
				position = extraData.icon2,
				joinDate = transferDate,
				joinDateDisplay = extraData.displaydate or transferDate,
			})
		end

		if (transfer.toteam ~= transfer.fromteam or transfer.role2 ~= transfer.role1) and
			(transfer.toteam ~= extraData.fromteamsec or transfer.role2 ~= extraData.role1sec) then
			-- primary transfer
			table.insert(transferList, {
				team = transfer.toteam,
				role = transfer.role2,
				position = extraData.icon2,
				joinDate = transferDate,
				joinDateDisplay = extraData.displaydate or transferDate,
			})
		end
	elseif transfer.toteam ~= extraData.fromteamsec or transfer.role2 ~= extraData.role1sec then
		-- classic transfer
		table.insert(transferList, {
			team = transfer.toteam,
			role = transfer.role2,
			position = extraData.icon2,
			joinDate = transferDate,
			joinDateDisplay = extraData.displaydate or transferDate,
		})
	end
end

function TeamHistoryAuto._completeTransfer(transfer)
	local leaveTransfers = mw.ext.LiquipediaDB.lpdb('transfer', {
		conditions = TeamHistoryAuto._buildConditions(transfer),
		order = 'date asc',
		query = 'toteam, role2, date, extradata'
	})

	for _, leaveTransfer in ipairs(leaveTransfers) do
		local extraData = leaveTransfer.extradata

		if
			(leaveTransfer.toteam ~= transfer.team or leaveTransfer.role2 ~= (transfer.role or '') or extraData.icon2 ~= transfer.position)
			and (extraData.toteamsec ~= transfer.team or extraData.role2sec ~= (transfer.role or '')) then

			transfer.leaveDate = LANG:formatDate('Y-m-d', leaveTransfer.date)
			transfer.leaveDateDisplay = extraData.displaydate or transfer.leaveDate

			break
		end
	end
end

function TeamHistoryAuto._buildConditions(transfer)
	local historicalNames = Team.queryHistoricalNames(transfer.team)

	local conditions = {
		'[[player::' .. _config.player .. ']]',
		'([[date::>' .. transfer.joinDate .. ']] OR [[date::' .. transfer.joinDate .. ']])'
	}

	if historicalNames then
		local fromCondition = Array.map(historicalNames, function(team) return '[[fromteam::' .. team .. ']]' end)
		fromCondition = '(' .. table.concat(fromCondition, ' OR ') .. ')'
		if transfer.role then
			fromCondition = fromCondition .. ' AND [[role1::' .. transfer.role .. ']]'
		elseif not _config.roleClean then
			fromCondition = fromCondition .. ' AND [[role1::]]'
		end

		local fromConditionSecondary = Array.map(historicalNames, function(team) return '[[extradata_fromteamsec::' .. team .. ']]' end)
		fromConditionSecondary = '(' .. table.concat(fromConditionSecondary, ' OR ') .. ')'
		if transfer.role then
			fromConditionSecondary = fromConditionSecondary .. ' AND [[extradata_role1sec::' .. transfer.role .. ']]'
		elseif not _config.roleClean then
			fromConditionSecondary = fromConditionSecondary .. ' AND [[extradata_role1sec::]]'
		end

		table.insert(conditions, '(' .. fromCondition .. ' OR ' .. fromConditionSecondary .. ')')
	elseif Table.includes(SPECIAL_ROLES_LOWER, (transfer.role or ''):lower()) then
		table.insert(conditions, '[[role1::' .. transfer.role .. ']] OR [[role1::' .. transfer.role:lower() .. ']]')
	end

	return table.concat(conditions, ' AND ')
end

function TeamHistoryAuto._buildLeaveDateDisplay(transfer)
	if transfer.leaveDateDisplay then return transfer.leaveDateDisplay end

	local lowerCasedRole = (transfer.role or ''):lower()
	if lowerCasedRole == 'Military' or not Table.includes(SPECIAL_ROLES_LOWER, (transfer.role or ''):lower()) then
		return '<span style="font-weight:bold;">Present</span>'
	end
	return ''
end

function TeamHistoryAuto._displayTransfer(transfer, teamCount)
	local leaveDateDisplay = TeamHistoryAuto._buildLeaveDateDisplay(transfer)

	local teamLink, teamText
	if String.isEmpty(transfer.team) and Table.includes(SPECIAL_ROLES_LOWER, (transfer.role or ''):lower()) then
		teamText = '<b>' .. transfer.role .. '</b>'
		transfer.role = nil
		transfer.position = nil
	elseif mw.ext.TeamTemplate.teamexists(transfer.team) then
		local leaveDateCleaned = TeamHistoryAuto._adjustDate(transfer.leaveDate)
		local teamData = mw.ext.TeamTemplate.raw(transfer.team, leaveDateCleaned --[[@as string]])
		teamLink = teamData.page
		teamText = '[[' .. teamLink .. '|' .. TeamHistoryAuto._buildTeamDisplayName(teamData) .. ']]'
	else
		teamLink = transfer.team
		teamText = '[[' .. transfer.team .. ']]'
	end

	if _config.store and (teamLink or transfer.role) then
		TeamHistoryAuto._storeLpdbDatapoint(teamCount, teamLink, transfer.joinDate, transfer.leaveDate, transfer.role)
	end

	if String.isNotEmpty(transfer.role) then
		if _config.roleConvert then
			local splitRole = mw.text.split(transfer.role, ' ')
			-- If we don't get a match, look at just the last word, there may be a prefix beforehand.
			transfer.role = _config.roleConvert[transfer.role:lower()] or _config.roleConvert[splitRole[#splitRole]:lower()] or transfer.role
		end

		if transfer.role == LOAN then
			teamText = '&#8250;&nbsp;' .. teamText
		end
		if _config.showRole then
			teamText = teamText .. '<span style="padding-left:3px; font-style:italic;">(' .. transfer.role .. ')</span>'
		end
	end

	local position = (transfer.position or ''):lower()
	local icon = _config.iconConvert and (_config.iconConvert[position] or _config.iconConvert['']) or nil

	return TeamHistoryAuto._printTransfer(transfer.joinDateDisplay, leaveDateDisplay, teamText, icon)
end

function TeamHistoryAuto._storeLpdbDatapoint(teamCount, teamLink, joinDate, leaveDate, playerRole)
	mw.ext.LiquipediaDB.lpdb_datapoint('Team_'..teamCount, {
		type = 'teamhistory',
		name = _config.player,
		information = teamLink,
		extradata = mw.ext.LiquipediaDB.lpdb_create_json({
			joindate = joinDate,
			leavedate = leaveDate or '2999-01-01',
			teamcount = teamCount,
			role = playerRole,
			auto = 1,
		})
	})
end

function TeamHistoryAuto._printTransfer(joinDate, leaveDate, teamText, icon)
	leaveDate = Logic.isNotEmpty(leaveDate) and (' &#8212; ' .. leaveDate) or nil

	return mw.html.create('tr')
		:tag('td')
			:addClass('th-mono')
			:css('float', 'left')
			:css('width', '50%')
			:css('font-style', 'italic')
			:wikitext(joinDate)
			:wikitext(leaveDate)
			:done()
		:tag('td')
			:css('float', 'right')
			:css('width', '50%')
			:wikitext(icon and (icon .. '&nbsp;') or nil)
			:wikitext(teamText)
			:done()
end

-- earlier date for fromteam to account for rebrands
function TeamHistoryAuto._adjustDate(date)
	if type(date) ~= 'string' or String.isEmpty(date) then
		return date
	end

	local year, month, day = date:match('(%d+)-(%d+)-(%d+)')
	date = os.time({day=day, month=month, year=year})
	return os.date('%Y-%m-%d', date - ONE_DAY)
end

function TeamHistoryAuto._buildTeamDisplayName(teamData)
	local teamName = teamData.name
	if string.len(teamName) <= 17 then
		return teamName
	end

	teamName = teamData.bracketname or teamData.name
	if string.len(teamName) <= 17 then
		return teamName
	end
	return teamData.shortname or teamData.name
end

return Class.export(TeamHistoryAuto)