Module:NewsUtil

From Leaguepedia | League of Legends Esports Wiki
Jump to: navigation, search

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', 'ChangedOnTeamRename', }

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 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)
	ret:set(playerData.Player:gsub('_', ' '), playerData)
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
		),
		{ 'League of Legends 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