Halo Esports Wiki

We are currently performing extensive maintenance to this wiki and as a result there will likely be errors. Please be patient while we work to fix all problems.

READ MORE

Halo Esports Wiki
Advertisement

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

local util_args = require('Module:ArgsUtil')
local util_cargo = require("Module:CargoUtil")
local util_html = require("Module:HtmlUtil")
local util_map = require('Module:MapUtil')
local util_source = require("Module:SourceUtil")
local util_table = require("Module:TableUtil")
local util_text = require("Module:TextUtil")
local util_title = require("Module:TitleUtil")
local util_time = require("Module:TimeUtil")
local util_toggle = require("Module:ToggleUtil")
local util_vars = require("Module:VarsUtil")
local i18n = require('Module:i18nUtil')
local OD = require('Module:OrderedDict')

local m_team = require('Module:Team')
local lang = mw.getLanguage('en')
local TabsDynamic = require('Module:TabsDynamic').constructor
local ContentByHeading = require('Module:ContentByHeading').constructor
local RoleList = require('Module:RoleList')
local Region = require('Module:Region')

local h = {}

local p = {}

p.COLUMNS = { 'DateDisplay', 'Region', 'Player', 'TeamStart', 'RoleStart', 'IsSubStart', 'IsTraineeStart', 'TeamEnd', 'RoleEnd', 'IsSubEnd', 'IsTraineeEnd', 'source_display' }

p.PLAYER_STATUSES = { 'Team', 'Role', 'IsSub', 'IsTrainee' }

-- these will be used to check for pre-post equality
-- since different objects can't be equal, we don't include Roles, RolesStaff, etc here
p.ALL_POSSIBLE_CHANGES = { 'Team', 'Role', 'IsSub', 'IsTrainee', 'Status', 'RoleModifier', }

local PLAYER_ARG_PARTS = { 'Player', 'Role', 'Status', 'LoanedFrom', 'LoanedTo', 'MoveType', 'Custom', 'ContractUntil', 'Assistance', 'Event', 'Replacing', 'Reason', 'Phase', 'Sub', 'Trainee', 'Rejoin', 'Order', 'SentenceGroup', 'LeaveDate', 'Unlinked', 'RemainFor', 'RemainForLink', 'AlreadyJoined', 'CurrentTeamPriority', 'SisterTeam', 'Reserve', }

p.CONTRACT_MAINTAINED_ON_LEAVE = {
	confirm_role_swap = true,
	confirm_role_swap_to_ingame = true,
	confirm_role_swap_from_ingame = true,
	end_official_sub = true,
	extended = true,
	from_academy = true,
	from_main = true,
	from_sister = true,
	gcd_end_official_sub = true,
	gcd_from_academy = true,
	gcd_from_main = true,
	gcd_loan_return = true,
	gcd_loaned_to = true,
	gcd_role_swap = true,
	gcd_role_swap_from_ingame = true,
	gcd_role_swap_to_ingame = true,
	gcd_to_academy = true,
	gcd_to_active = true,
	gcd_to_inactive = true,
	gcd_to_main = true,
	gcd_to_official_sub = true,
	gcd_to_starting = true,
	gcd_to_sub = true,
	leave_academy_as_temp_sub = true,
	leave_main_as_temp_sub = true,
	leave_sister_as_temp_sub = true,
	loan_return = true,
	loaned_to = true,
	opportunities = true,
	remain = true,
	role_swap = true,
	role_swap_from_ingame = true,
	role_swap_to_ingame = true,
	set_to_leave = true,
	set_to_leave_already_joined = true,
	to_academy = true,
	to_active = true,	
	to_inactive = true,
	to_main = true,
	to_official_sub = true,
	to_sister = true,
	to_starting = true,
	to_sub = true,
}

local VALID_INPUT_STATUSES = require('Module:NewsUtil/i18n').en
local LIST_OF_VALID_INPUT_STATUES = util_table.getKeys(VALID_INPUT_STATUSES)

function p.setId()
	-- news id set as global
	NEWS_ID = util_cargo.getUniqueLine(util_vars.setGlobalIndex('newsitem'))
end

function p.getId()
	return NEWS_ID or util_cargo.getUniqueLine(util_vars.getGlobalIndex('newsitem'))
end

function p.displayDate(str)
	local y, m, d = str:match('(%d%d%d%d)%-(%d%d)%-')
end

--------------------------------------------------------
function p.getPlayersFromArg(arg)
	local players = util_args.splitArgsArray(arg, PLAYER_ARG_PARTS)
	if not next(players) or not next(players[1]) then return OD() end
	return h.getPlayersGuaranteed(arg, players)
end

function p.getPlayerFromArg(arg)
	if not arg then return {} end
	local playerData = util_args.splitArgs(arg, PLAYER_ARG_PARTS)
	h.addPlayerData(nil, playerData)
	return playerData
end

function h.getPlayersGuaranteed(arg, players)
	local ret = OD()
	for _, playerData in ipairs(players) do
		h.addPlayerData(ret, playerData)
	end
	return ret
end

function h.addPlayerData(ret, playerData)
	if not playerData.Player then return end
	playerData.player = playerData.Player
	playerData.PlayerLink = util_title.target(playerData.Player)
	playerData.IsSub = util_args.castAsBool(playerData.Sub)
	playerData.IsTrainee = util_args.castAsBool(playerData.Trainee)
	local role = playerData.Role
	playerData.RoleSet = RoleList(role, { sub = playerData.IsSub, trainee = playerData.IsTrainee })
	playerData.Roles = playerData.RoleSet
	playerData.RolesIngame = playerData.RoleSet:ingame()
	playerData.RolesStaff = playerData.RoleSet:staff()
	
	playerData.Role = playerData.RoleSet:get(nil, {sep = '/'})
	playerData.RoleDisplay = playerData.RoleSet:names({len = 'name', sep='/', fulllength = true})
	
	playerData.RoleSortNumber = playerData.RoleSet:sortnumber()
	playerData.role = playerData.RoleSet
	playerData.sub = playerData.IsSub
	playerData.trainee = playerData.IsTrainee
	playerData.RoleModifier = p.getRoleModifierFromArgs(playerData, 'Sub', 'Trainee')
	playerData.Status = playerData.Status and playerData.Status:lower()
	h.validateStatus(playerData.Status)
	h.validateRole(role)
	if ret then
		ret:set(playerData.Player:gsub('_', ' '), playerData)
	end
end

function h.validateRole(role)
	if not role then return end
	if role:lower() == 'sub' or role:lower() == 'substitute' then
		error('|role=substitute is invalid, please use |role= |sub=yes instead')
	end
	if role:lower() == 'trainee' then
		error('|role=trainee is invalid, please use |role= |trainee=yes instead')
	end
end

function h.validateStatus(status)
	if not status then return end
	if not VALID_INPUT_STATUSES[status] then
		error(("Invalid status of %s. Please use one of the following: %s"):format(
			status,
			table.concat(LIST_OF_VALID_INPUT_STATUES, ', ')
		))
	end
end

--------------------------------------------------------

function p.getNewsCargoFieldsFromArgs(args)
	-- TODO: Separate this into two functions
	-- the first one should add "when-specific" fields
	-- the second should add only static, non-controversial fields
	local ret = {
		_table = 'NewsItems',
		Tournaments = util_map.splitAndConcat(
			args.tournaments or args.tournament,
			nil,
			util_title.target
		),
		Teams = util_map.splitAndConcat(
			args.teams or args.team,
			nil,
			m_team.teamlinkname
		),
		Date_Sort = args.date or util_vars.getVar('Date'),
		
		-- historically we wrote xxxx-xx-xx but we are standardizing with ? instead
		-- let's allow both inputs for backwards compatibility of what users are used to though
		Date_Display = args.display_date and args.display_date:gsub('x','?'),
		Region = Region(args.region),
		IsApproxDate = util_args.castAsBool(args.approx),
		Tags = args.tags,
		Sentence = args.Sentence,
		SentenceWithDate = h.getSentenceWithDate(args),
		Source = args.source,
		N_LineInDate = util_vars.setGlobalIndex('N_LineInDate'),
		NewsId = NEWS_ID,
		ExcludeFrontpage = util_args.castAsBool(args.no_frontpage),
		ExcludePortal = util_args.castAsBool(args.no_portal),
		Players = args.players or args.player,
	}
	return ret
end

function p.getExcludedPreloadsWhereCondition(list)
	local tbl = {
		h.getExcludedPreloadsToIgnoreCompletely(list),
		h.getExcludedPreloadsToIgnoreHalf(list.join, 'Join'),
		h.getExcludedPreloadsToIgnoreHalf(list.leave, 'Leave'),
	}
	return util_cargo.concatWhere(tbl)
end

function h.getExcludedPreloadsToIgnoreCompletely(list)
	return util_map.formatAndConcat(
		list,
		' AND ',
		'COALESCE(RC.Preload, News.Preload, "") != "%s"'
	)
end

function h.getExcludedPreloadsToIgnoreHalf(list, direction)
	if not list or #list == 0 then return nil end
	return ('RC.Direction!="%s" OR (%s)'):format(
		direction,
		h.getExcludedPreloadsToIgnoreCompletely(list)
	)
end

function p.getExcludedNewsPreloadsWhereCondition(list)
	if not list or #list == 0 then return nil end
	return util_cargo.concatWhere(
		util_map.format(
			list,
			'COALESCE(News.Preload,"") != "%s"'
		)
	)
end

function p.getRosterPortalDatesWhereCondition(period, dateFieldName)
	local ret = {
		('Dates.PeriodName="%s"'):format(period),
		('%s > Dates.DateStart'):format(dateFieldName),
		('%s < Dates.DateEnd'):format(dateFieldName),
	}
	return ret
end

--------------------------------------------------------

function p.getRCFieldsFromPlayerAndArgs(player, args)
	local ret = {
		_table = 'RosterChanges',
		Player = player.Player,
		Date_Sort = util_vars.getVar('Date'),
		Date_Display = args.date or args.Date_Sort,
		Source = args.source,
		Region = Region(args.region),
		CurrentTeamPriority = player.CurrentTeamPriority or args.team_priority,
		Team = m_team.teamlinkname(args.team),
		NewsId = p.getId(),
		Status = player.Status and player.Status:lower(),
		RoleModifier = p.getRoleModifierFromArgs(player, 'Sub', 'Trainee'),
		Role = player.Role,
		Roles = player.Roles,
		RolesIngame = player.RolesIngame,
		RolesStaff = player.RolesStaff,
		PlayerUnlinked = player.Unlinked,
		IsGCD = util_source.isGCD(args.Source),
	}
	return ret
end

function p.storeRosterChangesRow(row)
	row.RosterChangeId = h.setAndGetRosterChangeId(row)
	row.NewsId = row.NewsId or p.getId()
	util_cargo.store(row)
end

function h.setAndGetRosterChangeId(row)
	-- we need to make this encode enough data that it's impossible
	-- to have a case where an ID was changed and the page corresponding to it
	-- was NOT blank edited after the change.
	--
	-- because RO saves both player and team page and nothing else,
	-- including these two alongside a numerical counter is sufficient
	
	-- furthermore, we have to guarantee that when a page is split, the RC IDs remain static
	-- so let's also add a date param
	-- we don't want anything outside of this function to be called ever so first we'll
	-- check what the date was the last time we did this; if it changed then we need to
	-- reset our index, otherwise keep incrementing index and we're fine.
	local date = util_vars.getVar('Date')
	local lastDate = util_vars.getVar('rCPDate' .. row.Player .. (row.Team or '_'))
	util_vars.setVar('rCPDate' .. row.Player .. (row.Team or '_'), date)
	local index
	if lastDate and lastDate == date then
		index = util_vars.setGlobalIndex('rCP' .. date .. row.Player .. (row.Team or '_'))
	else
		index = util_vars.resetGlobalIndex('rCP' .. date .. row.Player .. (row.Team or '_'))
	end
	local tbl = {
		date,
		row.Player,
		row.Team or 'No Team',
		index
	}
	return util_table.concat(tbl, '_')
end

---------------------------------------------------------

-- for use when writing the date out as a sentence
-- wraps h.getDisplayDateForSentence for use outside of this module
-- renames args as expected from args instead of Cargo
function p.getDisplayDateForSentence(row)
	-- @param row: expects keys Date_Display, Date, and IsApproxDate
	-- @returns: Month D, with (approx.) if needed
	return h.getDisplayDateForSentence({
		display_date = row.Date_Display,
		approx = row.IsApproxDate,
		date = row.Date
	})
end

function h.getSentenceWithDate(args)
	if not args.Sentence then return nil end
	return ('%s, %s'):format(h.getDisplayDateForSentenceOnDataPage(args), args.Sentence)
end

function h.getDisplayDateForSentenceOnDataPage(args)
	return h.getDisplayDateForSentence({
		display_date = args.display_date,
		approx = args.approx,
		date = util_vars.getVar('Date')
	})
end

function h.getDisplayDateForSentence(data)
	-- @param data: expects keys display_date, date, and approx
	-- @returns: Month D, with (approx.) if needed
	if not data.display_date and not util_args.castAsBool(data.approx) then
		return lang:formatDate('F j', data.date)
	end
	return util_time.strToDateStrFuzzyWithoutYear(
		data.display_date or data.date,
		util_args.castAsBool(data.approx)
	)
end

-- for use when printing mmm YYYY dates with toggle to exact in data tables
function p.getDateAndRefDisplayForTable(row, when)
	if not (row['Date_Display' .. when] or row['Date' .. when]) then
		return nil
	end
	return ('%s%s'):format(
		p.getDateDisplayForTable(row, when),
		util_source.makeRef(row['Source' .. when]) or ''
	)
end

function p.getDateDisplayForTable(row, when)
	-- @param row: has keys Join/Leave for Date_Display, Date, and IsApproxDate
	-- @param when: "Join" or "Leave"
	-- @returns: mmm YYYY format date with approx equals sign as needed
	-- call this directly to avoid printing a ref immediately
	if not (row['Date_Display' .. when] or row['Date' .. when]) then
		return nil
	end
	local displays = {
		h.getToggleAndDisplay(row, when, 'approx', p.formatDateApproxForTableDisplay),
		h.getToggleAndDisplay(row, when, 'exact', h.formatDateExactForTableDisplay),
	}
	return util_table.concat(displays, '', tostring)
end

function h.getToggleAndDisplay(row, when, toggleName, f)
	return util_toggle.oflCellClasses(
		mw.html.create('span'):wikitext(f(h.getDisplayDateParams(row, when))),
		'date',
		toggleName
	)
end

function h.getDisplayDateParams(row, when)
	return row['Date_Display' .. when] or row['Date' .. when],
		row['IsApproxDate' .. when]
end

function p.formatDateApproxForTableDisplay(str, isApprox)
	if not str then return end
	local date = util_time.strToDateFuzzy(str)
	if not date.year then return '??? ????' end
	if not date.month then return '??? ' .. date.year end
	if not date.day then str = str:gsub('%?%?', '01') end
	return h.getApproxModifierForTable(isApprox) .. lang:formatDate('M Y', str)
end

function h.formatDateExactForTableDisplay(str, isApprox)
	if not str then return nil end
	return h.getApproxModifierForTable(isApprox) .. str
end

function h.getApproxModifierForTable(isApprox)
	return isApprox and '≈' or ''
end
-- end helper functions for p.getDateDisplayForTable

function p.getSentenceAndRefDisplay(row, when)
	local popup = util_toggle.popupButton(td)
	popup.inner:wikitext(row['Sentence' .. when])
		:wikitext(util_source.makeRef(row['Source' .. when]))
	popup.button:addClass('popup-ref-button')
	popup.wrapper:addClass('popup-ref-wrapper')
	popup.inner:addClass('popup-ref-inner')
	return tostring(popup.button)
end

function p.makeSentenceOutput(args, newsCargo)
	local tr = mw.html.create('tr')
	tr:addClass('news-data-sentence')
	local td = tr:tag('td')
		:attr('colspan', #p.COLUMNS)
		:addClass('news-data-sentence-cell')
	local div = td:tag('div')
		:addClass('news-data-sentence-div')
	div:tag('div')
		:wikitext(util_text.intLinkOrText(newsCargo.Subject))
		:wikitext(' - ')
		:addClass('news-data-sentence-wrapper')
		:wikitext(h.getSentenceWithDate(args))
	local button = h._printROButton(div, newsCargo)
	return tr
end

function p.printROButton(td, pagelist, team)
	local popup = util_toggle.popupButtonPretty(td)
	popup.button
		:addClass('news-data-ro')
		:attr('data-to-refresh', util_table.concat(pagelist.purge), ',')
		:attr('data-to-touch', util_table.concat(pagelist.touch), ',')
		:attr('data-ro-team', team)
	popup.wrapper:addClass('news-data-ro-wrapper')
	popup.inner:addClass('news-data-ro-inner')
	return popup.button
end

function h._printROButton(td, newsCargo)
	local pagelist = {
		purge = h.getPagesToRefresh(newsCargo),
		touch = h.getPagesToTouch(newsCargo),
	}
	p.printROButton(td, pagelist, newsCargo.Subject)
end

function h.getPagesToRefresh(newsCargo)
	return util_table.mergeArrays(
		util_map.arrayInPlaceAndMerge(
			{ 'Players', 'Teams', 'Tournaments' },
			h.getPagesFromKey,
			newsCargo
		),
		{ 'Halo Esports Wiki' }
	)
end

function h.getPagesToTouch(newsCargo)
	return h.getPagesFromKey('Players', newsCargo)
end

function h.getPagesFromKey(key, newsCargo)
	return util_text.splitNonempty(newsCargo[key])
end

function p.printEditButton(li, page)
	li:tag('div')
		:addClass('content-edit-button')
		:addClass('logged-in-link')
		:attr('data-href', page)
		:wikitext('e')
end

function p.sectionsOrTabs(byDate, threshold, tabs, headingLevel)
	headingLevel = headingSize or 3
	local total = 0
	for _, year in ipairs(byDate) do
		total = total + #byDate[year]
	end
	if total > threshold then return TabsDynamic(tabs, #tabs) end
	return ContentByHeading(tabs, headingLevel)
end

function p.notWhen(when)
	if when == 'Start' then return 'End' end
	return 'Start'
end

function p.getRoleModifierFromArgs(args, subArgName, traineeArgName)
	subArgName = subArgName or 'sub'
	traineeArgName = traineeArgName or 'trainee'
	if args[subArgName] and util_args.castAsBool(args[subArgName]) then
		return 'Sub'
	elseif args[traineeArgName] and util_args.castAsBool(args[traineeArgName]) then
		return 'Trainee'
	end
	return nil
end

return p
Advertisement