Module:PlayerTeamHistoryAbstract

From Call of Duty Esports Wiki
Jump to: navigation, search

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

local util_args = require('Module:ArgsUtil')
local util_cargo = require("Module:CargoUtil")
local util_esports = require("Module:EsportsUtil")
local util_html = require("Module:HtmlUtil")
local util_map = require("Module:MapUtil")
local util_math = require("Module:MathUtil")
local util_news = require("Module:NewsUtil")
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_timedelta = require("Module:TimedeltaUtil")
local util_vars = require("Module:VarsUtil")
local i18n = require('Module:i18nUtil')
local lang = mw.getLanguage('en')

local m_team = require('Module:Team')

local p = require('Module:GroupedRosterChangesAbstract'):extends()
local h = {}

function p:init(name)
	self:super('init', name)
	self.STATUSES_TO_IGNORE = {
		set_to_leave = true,
		opportunities = true,
	}
	self.PRELOADS_TO_IGNORE = {
		'extended', 'gcd_extended', 'remain', 'expire_notleave', 'set_to_leave', 'opportunities', 'set_to_leave_already_joined', 'gcd_expire_notleave', 're_sign', 'gcd_remove_notleave',
		leave = { 'retirement_no_team' },
		join = { 'gcd_expire_notleave_yet', },
	}
	self.INCLUDE_STATUS = false
	self.DEBUG = false
	self.CONTRACT_DATES = {}	
end

function p:run(args)
	util_cargo.setStoreNamespace('')
	h.castArgs(args)
	self:setConstants(args)
	self:setDebugIfEnabled()
	return self:super('run', args)
end

function h.castArgs(args)
	args.player = util_args.strOrTitle(args.player)
end

function p:setConstants(args)
	if util_args.castAsBool(args.debug) then
		self.DEBUG = true
	end
end

function p:setDebugIfEnabled()
	if self.DEBUG then
		self.COLUMNS[#self.COLUMNS+1] = 'KeyJoin'
		self.COLUMNS[#self.COLUMNS+1] = 'KeyLeave'
		self.COLUMNS[#self.COLUMNS+1] = 'PreloadJoin'
		self.COLUMNS[#self.COLUMNS+1] = 'PreloadLeave'
		self.COLUMNS[#self.COLUMNS+1] = 'SisterTeamPage'
		self.COLUMNS[#self.COLUMNS+1] = 'Status'
		-- self.COLUMNS[#self.COLUMNS+1] = 'RoleModifier'
		-- self.COLUMNS[#self.COLUMNS+1] = 'NextTeam'
		self.COLUMNS[#self.COLUMNS+1] = 'changeIndex'
	end
end

function p:getTables()
	local ret = {
		'NewsItems__Players',
		'NewsItems=News',
		'PlayerRedirects=PR',
		'RosterChanges=RC',
		'PlayerRenames=RN',
		'ResidencyChanges=ResChange',
		'Contracts',
		'TeamRedirects=TRed1', -- join from contracts to ST1
		'TeamRedirects=TRed2', -- join from news to ST2
		'SisterTeams=ST1', -- the sister team page (if one exists) of the contract team
		'SisterTeams=ST2', -- the sister team page (if one exists) of the news team
	}
	return ret
end

function p:getJoin()
	-- remember this join is JUST for retrieving fields
	-- so we dont have to worry about one-to-many conditions really, because nothing is happening
	-- in any where conditions
	-- so all of the redirects and stuff just discovering the target
	-- not then also joining to another field making a many:many
	local ret = {
		-- joins to associate all of the relevant news in order
		'NewsItems__Players._rowID=News._ID',
		'NewsItems__Players._value=PR.AllName',
		'News.NewsId=RC.NewsId',
		'News.NewsId=RN.NewsId',
		'News.NewsId=ResChange.NewsId',
		'News.NewsId=Contracts.NewsId',
		
		-- contracts need to respect contracts from the same org
		'Contracts.Team=TRed1.AllName',
		'TRed1._pageName=ST1.Team',
		
		-- roster changes need to discover the org so we can pull contracts
		'RC.Team=TRed2.AllName',
		'TRed2._pageName=ST2.Team',
	}
	return ret
end

function p:getWhere(args)
	local tbl = {
		('PR._pageName="%s"'):format(args.player),
		util_news.getExcludedPreloadsWhereCondition(self.PRELOADS_TO_IGNORE),
	}
	util_table.mergeArrays(tbl, h.getCorrectPlayerWhereConditions())
	return util_cargo.concatWhere(tbl)
end

function h.getCorrectPlayerWhereConditions()
	local tablesNeedingConditions = { 'RC', 'Contracts' }
	return util_map.inPlace(tablesNeedingConditions, h.getOneCorrectPlayerWhereCondition)
end

function h.getOneCorrectPlayerWhereCondition(tbl)
	return ('%s.Player=NewsItems__Players._value OR %s.Player IS NULL'):format(tbl, tbl)
end

function p:getFields()
	local fields = self:super('getFields')
	local newFields = {
		'RN.OriginalName=NameOld',
		'ResChange.ResidencyOld=ResidencyOld',
		'News._pageName=ResPageName',
		'Contracts.ContractEnd',
		'Contracts.IsRemoval=IsContractRemoval',
		'TRed2._pageName=TeamPage',
		
		-- this originally was ST2._pageName before Contracts.Team but i think that's wrong
		-- im changing it despite not finding any bug affecting it
		-- but im 99% sure there's only no bug, because all teams with contracts happen to have sister teams too
		'COALESCE(ST1._pageName, Contracts.Team, ST2._pageName, RC.Team)=SisterTeamPage',
	}
	return util_table.mergeArrays(fields, newFields)
end

function p:getGroupBy()
	return 'COALESCE(RC.RosterChangeId, News.NewsId)'
end

-- format raw data rows
function p:formatOneNonRCRow(row)
	if row.IsContractRemoval then
		self.CONTRACT_DATES[row.SisterTeamPage] = nil
	end
	if not row.ContractEnd then return end
	self.CONTRACT_DATES[row.SisterTeamPage] = row.ContractEnd
end

function p:formatOneRawDataRow(row)
	self:super('formatOneRawDataRow', row)
	
	-- dealing with contracts here will bring information *forward* when a player is on same sister team
	-- and we want to preserve the fact that they had a contract already
	-- this construction is done as we sort through the changes and apply formatting
	-- later on we'll go back through the entire thing a 2nd time to do "after-the-fact" information
	-- which will apply "normal" contract information, when contract info is stored *after* the
	-- joining-the-team roster change in question
	
	-- because maps happen forward, we can just do this in order
	-- CONTRACT_DATES is the repository of info we maintain moving forward
	local potentialContractDate = self.CONTRACT_DATES[row.SisterTeamPage]
	if row.Date_SortJoin and potentialContractDate and potentialContractDate > row.Date_SortJoin then
		row.ContractEnd = potentialContractDate
	end
end

function p:getKey(row)
	local key = ('%s%s%s%s'):format(
		row.Team or '',
		row.RoleModifier or 'Normal',
		row.Role or 'Unknown',
		self:getAndSetMeaningfulStatus(row)
	)
	return key
end

function p:getAndSetMeaningfulStatus(row)
	if self.STATUSES_TO_IGNORE[row.Status] then
		row.Status = nil
	end
	return row.Status or ''
end

-- group stuff
function p:updateAncillaryInformation(changesByLine, queryRow)
	-- this is now propagating stuff backwards
	-- for each new line we add, we go back through our entire output and check if we want to update anything
	-- this has to be done in order, not all at once in the end, because information changes over time
	-- so for the most part we'll go back and say, "what teams is the player CURRENTLY on at time of news?"
	-- and then update this piece of information (e.g. residency change) on all of them
	-- but we'll leave any rows alone that already have departures
	for _, outputRow in ipairs(changesByLine) do
		h.updateRowAncillaryInformationIfNeeded(outputRow, queryRow)
	end
end

function h.updateRowAncillaryInformationIfNeeded(outputRow, queryRow)
	if not outputRow.TeamLeave then
		-- information that needs to get updated when the player is CURRENTLY on
		-- the team. This includes contract information.
		h.updateAncillaryInformationOnCurrentTeamsIfNeeded(outputRow, queryRow)
		
		-- do not continue to do the only-after-they-left stuff
		return
	end
	
	-- these are PRE-CHANGE values, so when they occur it means only apply them
	-- when you already left the team
	-- if you're still on the team, either the NEXT old one will apply, or the one in infobox will apply
	for _, v in ipairs({ 'NameOld', 'ResidencyOld', }) do
		h.updateAncillaryValueInRow(outputRow, queryRow, v)
	end
end

function h.updateAncillaryInformationOnCurrentTeamsIfNeeded(outputRow, queryRow)
	-- this is part of the propagating information backward process
	if queryRow.ContractEnd and queryRow.SisterTeamPage == outputRow.SisterTeamPage then
		outputRow.ContractEnd = queryRow.ContractEnd
	end
end

function h.updateAncillaryValueInRow(outputRow, queryRow, value)
	-- don't overwrite. also it's possible value is nil but whatever.
	outputRow[value] = outputRow[value] or queryRow[value]
end

function p:isOriginalNews(changesByLine, row, index)
	if not index then return true end
	if not index then return true end
	for _, oldChangeLineKey in ipairs(index) do
		local oldChange = self:getOldChangeFromIndex(changesByLine, oldChangeLineKey)
		if h.isThisLineARepeatOfAnOldOne(row, oldChange) then
			return false
		end
	end
	return true
end

function h.isThisLineARepeatOfAnOldOne(row, oldRow)
	if row.TeamJoin and not oldRow.TeamLeave then
		return h.teamsWhenAreIdentical(row, oldRow, 'Join')
	end
	return false
end

function h.teamsWhenAreIdentical(row, oldRow, when)
	for _, v in ipairs({ 'Team', 'RoleModifier', 'Role', 'Status' }) do
		if row[v .. when] ~= oldRow[v .. when] then
			return false
		end
	end
	return true
end

function p:doWeNeedANewLine(changesByLine, row, index)
	if not row.TeamLeave then return true end
	return not index
end

function p:getOldChangeFromIndex(changesByLine, oldChangeLineKey)
	return changesByLine[oldChangeLineKey.changeNumber]
end

function p:getMostRecentLineNumberFromIndex(index)
	return index[#index].changeNumber
end

function p:updateIndex(changesByLine, row, index)
	index[#index+1] = { changeNumber = #changesByLine }
end

function p:addNewTeamToExistingLine(line, row)
	-- we earlier split stuff into separate start/end so we should be fine to just merge now
	if line.TeamLeave then return end
	self:super('addNewTeamToExistingLine', line, row)
end

-- formatting
function p:formatRowForOutput(row)
	row.PlayerLinked = util_esports.playerLinked(row.Player)
	row.RoleDisplay = row.Roles:images{ sep="", size=15 }
	row.TeamDisplay = self:getTeamDisplay(row)
	row.RegionDisplay = row.Region:image()
	row.DateLeaveDisplay = self:getDateDisplay(row, 'Leave')
	row.DateJoinDisplay = self:getDateDisplay(row, 'Join')
	row.DurationDisplay = util_timedelta.approxDurationDisplay(self:getDurationArgs(row))
	self:getStatusAndUpdateFlag(row)
	self:addSortKeys(row)
	h.correctMissingDates(row)
end

function p:getTeamDisplay(row) end

function p:getDateDisplay(row, when) end

function p:getDurationArgs(row)
	local tbl = {
		row.DateJoin or 'xxxx-xx-xx',
		row.DateLeave or os.date('%Y-%m-%d'),
		row.IsApproxDateJoin or row.IsApproxDateLeave,
	}
	return unpack(tbl)
end

function p:getStatusAndUpdateFlag(row) end

function h.correctMissingDates(row)
	if not row.DateJoinDisplay then
		row.DateJoinDisplay = '??? ????'
		h.addSortKey(row, 'DateJoinDisplay', -1)
	end
	if not row.DateLeaveDisplay then
		row.DateLeaveDisplay = ("''%s''"):format(i18n.print('present'))
		h.addSortKey(row, 'DateLeaveDisplay', 9999999999999)
	end
end

function p:addSortKeys(row)
	h.addSortKey(row, 'DateJoinDisplay', row.index)
	h.addSortKey(row, 'TeamDisplay', row.Team)
	h.addSortKey(
		row,
		'DateLeaveDisplay',
		util_time.unix(row.Date_SortLeave)
	)
	h.addSortKey(
		row,
		'DurationDisplay',
		util_timedelta.approxDurationSeconds(self:getDurationArgs(row))
	)
end

function h.addSortKey(row, col, sortkey)
	if not row[col] then return end
	row[col .. '_Sort'] = sortkey
end

function p:setEarlierRowNextTeamValues(changesByLine, queryRow)
	if queryRow.Direction == 'Leave' then return end
	for _, outputRow in ipairs(changesByLine) do
		if not outputRow.TeamLeave then
			-- we can't add a NextTeam if they're still on the team
		else
			outputRow.NextTeam = outputRow.NextTeam or queryRow.Team
			outputRow.changeIndexNext = 'queryRow.index'
		end
	end
end

-- cargo STORE
function p:storeCargo(changesByLine) end

-- output
function p:makeOutput(changesByLine)
	self:setColumnsBasedOnIncludedInformation()
	local output = mw.html.create()
	local tbl = output:tag('table')
		:addClass('player-team-history')
		:addClass('hoverable-rows')
		:addClass('sortable')
	self:printHeader(tbl)
	util_map.selfRowsInPlace(self, changesByLine, p.printOneRow, tbl)
	return output
end

function p:printHeader(tbl)
	local tr = tbl:tag('tr')
	for _, col in ipairs(self.COLUMNS) do
		tr:tag('th')
			:wikitext(i18n.print(col))
			:attr('data-sort-type', self.COLUMNS.sorttypes[col])
			:addClass(self.COLUMNS.colclasses[col])
	end
end

function p:setColumnsBasedOnIncludedInformation()
	if self.INCLUDE_STATUS then
		self.COLUMNS[#self.COLUMNS+1] = 'StatusDisplay'
	end
	self.COLUMNS.classes[self.COLUMNS[#self.COLUMNS]] = 'roster-portal-lastcell'
end

function p:printOneRow(row, tbl)
	local tr = tbl:tag('tr')
	for _, col in ipairs(self.COLUMNS) do
		self:printOneCell(tr, row, col, self.COLUMNS)
	end
	self:printEditButtons(util_html.lastChild(tr), row)
end

function p:printOneCell(tr, row, col)
	tr:tag('td')
		:wikitext(row[col])
		:addClass(self.COLUMNS.classes[col])
		:attr('data-sort-value', row[col .. '_Sort'])
end

function p:printEditButtons(td, row) end

return p