Leaguepedia | League of Legends Esports Wiki

Documentation for this module may be created at Module:TeamMembers/doc

local util_args = require('Module:ArgsUtil')
local util_cargo = require("Module:CargoUtil")
local util_esports = require("Module:EsportsUtil")
local util_data = require("Module:DataUtil")
local util_form = require("Module:FormUtil")
local util_html = require("Module:HtmlUtil")
local util_infobox = require("Module:InfoboxUtil")
local util_map = require("Module:MapUtil")
local util_news = require("Module:NewsUtil")
local util_sort = require("Module:SortUtil")
local util_source = require("Module:SourceUtil")
local util_table = require("Module:TableUtil")
local util_text = require("Module:TextUtil")
local util_time = require("Module:TimeUtil")
local util_title = require("Module:TitleUtil")
local util_toggle = require("Module:ToggleUtil")
local util_vars = require("Module:VarsUtil")
local i18n = require('Module:i18nUtil')

local m_team = require('Module:Team')
local Region = require('Module:Region')
local RoleList = require('Module:RoleList')

local DEBUG = false

local ARGS = { 'Player', 'Country', 'Residency', 'IrlName', 'Role', 'DateJoin', 'DateLeave', 'ContractEnd', 'SortKey' }

local SETTINGS = require('Module:TeamMembers/Settings')

local TOGGLES_DATE = {
	order = { 'approx', 'exact' },
	key = 'date'
}

local h = {}

local p = {}
function p.fromCargo(frame)
	i18n.init('TeamMembers', 'NewsUtil')
	local args = util_args.merge()
	h.castArgs(args)
	h.setConstants(args)
	h.setColumns()
	util_toggle.oflInit(TOGGLES_DATE)
	local listOfChanges = h.queryForChanges(args.team or mw.title.getCurrentTitle().text, args)
	h.processChangesRows(listOfChanges)
	local players = h.computePlayers(listOfChanges)
	return h.getPlayerInfoAndMakeOutput(players, args)
end

function p.fromArgs(frame, args)
	i18n.init('TeamMembers')
	local args = util_args.merge()
	h.castArgs(args)
	h.setConstants(args)
	h.setColumns()
	util_toggle.oflInit(TOGGLES_DATE)
	local players = h.getPlayersFromArgs(args)
	return h.getPlayerInfoAndMakeOutput(players)
end

-- private
function h.castArgs(args)
	args.team = args.team and m_team.teamlinkname(args.team)
	args.debug = util_args.castAsBool(args.debug)
	if not args.when then args.when = 'current' end
end

function h.setConstants(args)
	DEBUG = args.debug
	if not SETTINGS[args.when] then
		error(i18n.print('error_InvalidWhen'))
	end
	SETTINGS = SETTINGS[args.when]
end

function h.setColumns()
	if DEBUG then SETTINGS.columns[#SETTINGS.columns+1] = '_pageName' end
end

-- from cargo
function h.queryForChanges(team, args)
	return util_cargo.queryAndCast(h.getChangesQuery(team, args))
end

function h.getChangesQuery(team, args)
	local query = {
		tables = {
			'TeamRedirects=TR',
			'TenuresUnbroken=Tenures',
			'TenuresUnbroken__RosterChangeIds=RCID',
			'RosterChanges=RC',
			'NewsItems=News',
			
			-- this PR is for a field, not a join
			'PlayerRedirects=PR',
		},
		join = {
			'TR.AllName=Tenures.Team',
			'Tenures._ID=RCID._rowID',
			'RCID._value=RC.RosterChangeId',
			'RC.NewsId=News.NewsId',
			'Tenures.Player=PR.AllName',
		},
		where = h.getChangesWhere(team, args),
		fields = h.getChangesFields(),
		oneToMany = h.getChangesOneToMany(),
		orderBy = 'PR.AllName, News.Date_Sort ASC, TenuresPrimaryKey ASC',
	}
	return query
end

function h.getChangesWhere(team, args)
	local tbl = {
		('TR._pageName="%s"'):format(team),
		SETTINGS.where,
	}
	-- ugly workaround for https://phabricator.wikimedia.org/T232190
	if team == "Storm Teams" then
		tbl[1] = ('TR._pageName=CONCAT("Storm T", "eams")')
	end
	return util_cargo.concatWhere(tbl)
end

function h.getChangesFields()
	local ret = {
		-- PlayerKey is needed here so we have a standardized way of matching up with
		-- PR.AllName in the playerExtraInfo field later on
		-- this is due to Cargo treating modified unicode letters the same as
		-- their non-unicode varieties
		'PR.AllName=PlayerKey',
		'Tenures.Player',
		'Tenures.NameLeave=Name',
		'Tenures._ID=TenuresPrimaryKey',
		'Tenures.DateJoin=DateJoin',
		'Tenures.DateLeave=DateLeave',
		'Tenures.ContractEnd',
		'Tenures.ResidencyLeave=Residency [region]',
		'Tenures.NameLeave=Name',
		'Tenures.NextTeam',
		'Tenures.NextIsRetired [boolean]',
		'Tenures.NextIsWildrift [boolean]',
		'Tenures.NextIsDeceased [boolean]',
		'Tenures.IsCurrent [boolean]',
	}
	return ret
end

function h.getChangesOneToMany()
	local oneToMany = {
		groupBy = { 'TenuresPrimaryKey' },
		fields = {
			RosterChanges = {
				'RC.RoleModifier',
				'RC.RolesIngame__full=RolesIngame',
				'RC.Roles__full=Roles',
				'RC.Status',
				'RC.Direction',
				'RC.NewsId', -- for debugging
				'News.Date_Display',
				'News.Date_Sort',
				'News.IsApproxDate [boolean]',
				'News.Source',
				'News.Sentence',
			}
		}
	}
	return oneToMany
end

function h.processChangesRows(listOfChanges)
	util_map.rowsInPlace(listOfChanges, h.processOneChangeRow)
end

function h.processOneChangeRow(row)
	h.moveRosterChangesToExpectedNames(row)
	
	-- only completely unambiguous constants should be handled here
	-- anything that can change before/after needs to be gotten from the LAST roster change
	-- in the function below this
	row.SortKeyName = row.Name:lower()
	row.SortKeyLeave = row.DateLeave
	row.DateDisplayJoin = h.getDateDisplay(row, 'Join')
	row.DateDisplayLeave = h.getDateDisplay(row, 'Leave')
end

function h.moveRosterChangesToExpectedNames(row)
	if not row.RosterChanges or not row.RosterChanges[1] then return end
	if row.RosterChanges[1].Direction == 'Join' then
		row.Date_DisplayJoin = row.RosterChanges[1].Date_Display
		row.Date_SortJoin = row.RosterChanges[1].Date_Sort
		row.IsApproxDateJoin = row.RosterChanges[1].IsApproxDate
		row.SourceJoin = row.RosterChanges[1].Source
		row.SentenceJoin = row.RosterChanges[1].Sentence
	end
	
	local last = row.RosterChanges[#row.RosterChanges]
	row.RoleModifier = last.RoleModifier
	row.Status = last.Status
	
	-- we could change this to be a RoleList, or we can just listen to the ingame/all
	-- params that we got from db
	-- either way is fine, and i don't see any inherent advantage to one over the other
	row.RolesIngame = RoleList(last.RolesIngame, { modifier = row.RoleModifier })
	row.Roles = RoleList(last.Roles, { modifier = row.RoleModifier })
	if row.Roles:hasIngame() then
		row.SortKeyRole = row.RolesIngame:sortnumber()
	else
		row.SortKeyRole = row.Roles:sortnumber()
	end
	
	if last.Direction == 'Leave' then
		row.Date_DisplayLeave = last.Date_Display
		row.Date_SortLeave = last.Date_Sort
		row.IsApproxDateLeave = last.IsApproxDate
		row.SourceLeave = last.Source
		row.SentenceLeave = last.Sentence
	end
end

function h.getDateDisplay(row, when)
	return ('%s<span class="team-members-ref">%s</span>'):format(
		util_news.getDateDisplayForTable(row, when) or '',
		util_news.getSentenceAndRefDisplay(row, when)
	)
end

function h.getRefSentencePopup(row, when)
	local popup = util_toggle.popupButton()
	popup.inner:wikitext(row['Sentence' .. when])
		:addClass('team-members-sentence')
	popup.wrapper:addClass('team-members-sentence-wrapper')
	popup.span:addClass('team-members-sentence-span')
	return tostring(popup.span)
end

function h.computePlayers(listOfChanges)
	local players = { keys = {} }
	for _, row in ipairs(listOfChanges) do
		if h.isAPlayer(row) then
			util_table.push(players.keys, row.Player)
			if not row.PlayerKey then
				error(i18n.print('error_MissingRedirects', row.Player))
			end
			row.PlayerKey = mw.ustring.lower(row.PlayerKey)
			players[#players+1] = row
		end
	end
	return players
end

function h.isAPlayer(row)
	-- this could end up more complex later
	if row.Status == 'official_sub' or row.RolesIngame:exists() then
		return true
	end
end

-- from args
function h.getPlayersFromArgs(args)
	local arr = util_args.splitArgsArray(args.members, ARGS)
	local players = { keys = {} }
	for _, row in ipairs(arr) do
		local key = util_title.escape(mw.ustring.lower(row.Player))
		util_table.push(players.keys, key)
		row.PlayerKey = key
		players[#players+1] = row
	end
	return players
end

-- merge from cargo and from args
function h.getPlayerInfoAndMakeOutput(players, args)
	if not next(players.keys) then return h.makeNoResultsOutput() end
	local playerExtraInfo = h.queryForPlayerExtraInfo(players)
	util_map.unorderedDictRowsInPlace(playerExtraInfo, h.formatOnePlayerExtraInfo)
	util_map.rowsInPlace(players, h.formatOnePlayer)
	h.addInfoToPlayers(players, playerExtraInfo)
	util_map.rowsInPlace(players, h.formatOneFinalPlayer)
	util_sort.tablesByKeys(players, SETTINGS.sort_fields, SETTINGS.sort_ascending)
	util_data.removeUnusedColumns(SETTINGS.columns, players)
	return h.makeNotice(args), h.makeOutput(players)
end

function h.makeNoResultsOutput()
	if util_vars.getBool('isdisbanded') then return '<!-- -->' end
	return i18n.print('noResultsText')
end

-- Output

function h.queryForPlayerExtraInfo(players)
	local dict = util_cargo.getRowDict(h.getPlayerExtraInfoQuery(players), 'PlayerKey')
	return h.normalizePlayerExtraInfo(dict)
end

function h.getPlayerExtraInfoQuery(players)
	local query = {
		tables = {
			'PlayerRedirects=PR',
			'Players=P',
			'Alphabets=A',
		},
		join = {
			'PR._pageName=P._pageName',
			'P.NameAlphabet=A.Alphabet',
		},
		fields = h.getPlayerExtraInfoFields(),
		where = h.getPlayerExtraInfoWhere(players),
	}
	return query
end

function h.getPlayerExtraInfoFields()
	local ret = {
		-- matches the PlayerKey from earlier
		'PR.AllName=PlayerKey',
		'COALESCE(P.NationalityPrimary,P.Country)=Country [country]',
		'P.Name=IrlName',
		'P.NativeName',
		'P.Residency [region]',
		'P.Team=CurrentTeam',
		'P._pageName=PlayerPage',
		'A.IsTransliterated[boolean]'
	}
	return ret
end

function h.getPlayerExtraInfoWhere(players)
	return util_cargo.concatWhereOr(
		util_map.format(
			players.keys,
			'PR.AllName="%s"'
		)
	)
end

function h.formatOnePlayerExtraInfo(row)
	row.Contract = row.Contract or '-'
end

function h.formatOnePlayer(row)
	row.PlayerDisplay = util_esports.playerLinked(row.Name)
end

function h.normalizePlayerExtraInfo(dict)
	local normalized = {}
	for k, v in pairs(dict) do
		normalized[mw.ustring.lower(k)] = v
	end
	return normalized
end

-- add info to players
function h.addInfoToPlayers(players, playerExtraInfo)
	-- PlayerKey in both cases is from PlayerRedirects
	-- so we'll guarantee in both cases get the PR version of the name
	-- we also lowercased it earlier, but that shouldn't actually matter
	for _, playerData in ipairs(players) do
		-- if not playerExtraInfo[playerData.PlayerKey] then
			-- util_vars.log(playerData.PlayerKey)
		-- end
		util_table.mergeDontOverwrite(playerData, playerExtraInfo[playerData.PlayerKey])
	end
end

function h.formatOneFinalPlayer(row)
	row.classes = {}
	row.Country = row.Country:image()
	row.Residency = row.Residency:image()
	row.RoleDisplay = h.getRoleDisplay(row)
	row.IrlNameDisplay = h.getIrlNameDisplay(row)
	h.addNextTeamDisplay(row)
	local teamHistPopup = h.getTeamHistPopup(row.Player)
	row.NextTeamDisplay = row.NextTeamDisplay .. teamHistPopup
	row.DateDisplayJoinWithPopup = h.concatDateDisplayAndPopup(row.DateDisplayJoin, teamHistPopup)
	if row.ContractEnd and not util_time.dateIsInFuture(row.ContractEnd) then
		row.classes.ContractEnd = 'team-members-expired'
	end
	row.attrs = {
		PlayerDisplay = {
			['data-player-id'] = row.Name,
		},
		RoleDisplay = {
			['data-sort-value'] = row.SortKeyRole,
		},
		DateDisplayJoinWithPopup = {
			['data-sort-value'] = util_time.unix(row.Date_SortJoin)
		},
		DateDisplayJoin = {
			['data-sort-value'] = util_time.unix(row.Date_SortJoin)
		},
		DateDisplayLeave = {
			['data-sort-value'] = util_time.unix(row.Date_SortLeave)
		},
	}
	row.classes.DateDisplayJoinWithPopup = 'team-members-date-with-popup'
end

function h.getRoleDisplay(row)
	local role = row.RolesIngame:flairs{len='role'} or row.Roles:flairs{len='role'}
	if not row.Status then return role end
	local toggle = util_toggle.allToggleAll(nil, 'member-statuses')
	toggle.button:wikitext('(+) ')
	toggle.button2:wikitext('(–) ')
	toggle.content:wikitext((' %s'):format(i18n.print(row.Status)))
	return role .. ' ' .. tostring(toggle.tbl)
end

function h.getIrlNameDisplay(row)
	if not row.NativeName then return row.IrlName end
	if not row.IrlName then return nil end
	if not row.IsTransliterated then return row.IrlName end
	return ('%s (%s)'):format(row.IrlName, row.NativeName)
end

function h.addNextTeamDisplay(row)
	if row.NextIsDeceased then
		row.NextTeamDisplay = m_team.rightshort('deceased')
		return
	end
	if row.NextIsWildrift then
		row.NextTeamDisplay = m_team.rightshort('wild rift')
		return
	end
	if row.NextIsRetired then
		row.NextTeamDisplay = m_team.rightshort('retired')
		return
	end
	row.NextTeam = row.NextTeam or row.CurrentTeam
	if row.NextTeam then
		row.NextTeamDisplay = m_team.rightshortlinked(row.NextTeam)
		return
	end
	row.classes.NextTeamDisplay = 'newteam-none'
	row.NextTeamDisplay = 'None'
end

function h.getTeamHistPopup(player)
	local button = util_toggle.popupButtonLazy(
		nil,
		'tmtimeline',
		('PlayerTeamHistoryPopup|%s'):format(player)
	)
	return tostring(button.span)
end

-- 
function h.concatDateDisplayAndPopup(date, popup)
	local output = mw.html.create()
	output:tag('div')
		:addClass('team-members-date-container')
		:wikitext(date)
	output:wikitext(popup)
	return tostring(output)
end

-- output
function h.makeNotice(args)
	if args.when ~= 'current' then return '' end
	return mw.getCurrentFrame():expandTemplate{title = 'ContractExpirationNotice' }
end

function h.makeOutput(players)
	local output = mw.html.create()
	util_infobox.printLowContentNoticeIfNeeded(output, i18n.print('lowContentNotice'))
	h.printToggler(output)
	local tbl = output:tag('table')
		:addClass('wikitable')
		:addClass('sortable')
		:addClass('team-members')
		:addClass('hoverable-rows')
		:addClass(SETTINGS.parent_class)
	util_html.printHeaderFromI18n(tbl, SETTINGS.columns)
	h.printRows(tbl, players)
	return output
end

function h.printToggler(output)
	local div = output:tag('div')
		:addClass('toggle-button')
	div:wikitext(i18n.print('toggleDatesIntro'))
	util_toggle.printOptionFromListTogglers(div, TOGGLES_DATE)
end

function h.printRows(tbl, players)
	for _, row in ipairs(players) do
		local tr = util_html.printRowByList(tbl, row, SETTINGS.columns)
		h.printRefreshButton(util_html.lastChild(tr), row.PlayerPage)
	end
end

function h.printRefreshButton(td, player)
	td:addClass('lastcell')
	local div = td:tag('div')
		:addClass('lastcell-action-pretty')
		:addClass('team-members-refresh')
		:attr('data-player', player)
end

return p