Module:NewsUtil

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 = { from_sister = true, to_sister = true, from_academy = true, to_academy = true, from_main = true, to_main = true, gcd_from_academy = true, gcd_from_main = true, gcd_to_academy = true, gcd_to_main = true, opportunities = true, set_to_leave = true, set_to_leave_already_joined = true, to_inactive = true, to_active = true, remain = true, role_swap_from_ingame = true, role_swap_to_ingame = true, leave_main_as_temp_sub = true, leave_academy_as_temp_sub = true, leave_sister_as_temp_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'):gsub('XX', '01'):gsub('xx', '01') end local res, formattedDate = pcall(lang.formatDate, lang, 'M Y', str) if not res then error(('Yikes! %s'):format(str)) end return h.getApproxModifierForTable(isApprox) .. formattedDate 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