Edit the documentation or categories for this module. This module has an i18n file.
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', 'LoanedFrom', 'LoanedTo', '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', 'Date', 'Unlinked', 'RemainFor', 'RemainForLink', 'AlreadyJoined', 'CurrentTeamPriority', 'SisterTeam', 'Reserve', 'Deceased', }
p.JOIN_IS_IN_FUTURE = {
set_to_join = true,
}
p.CONTRACT_MAINTAINED_ON_LEAVE = {
confirm_loaned_from = true,
confirm_loaned_to = true,
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_from_sister = 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_sister = 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,
team_rename_same_page = 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)
playerData.IsDeceased = util_args.castAsBool(playerData.Deceased)
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),
ExcludeArchive = util_args.castAsBool(args.no_archive),
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 h.getPeriodWhereCondition(period, dateFieldName)
if period == nil then return {} end
return {
('Dates.PeriodName="%s"'):format(period),
('%s > Dates.DateStart'):format(dateFieldName),
('%s < Dates.DateEnd'):format(dateFieldName),
}
end
function h.getDatesWhereCondition(dateStart, dateEnd, dateFieldName)
if dateStart == nil then return {} end
local ret = {
("%s > '%s'"):format(dateFieldName, dateStart)
}
if dateEnd ~= nil then
util_table.mergeArrays(ret, { ("%s < '%s'"):format(dateFieldName, dateEnd) })
end
return ret
end
function p.getRosterPortalDatesWhereCondition(period, dateFieldName, dateStart, dateEnd)
periodWhere = h.getPeriodWhereCondition(period, dateFieldName)
datesWhere = h.getDatesWhereCondition(dateStart, dateEnd, dateFieldName)
return util_table.mergeArrays(periodWhere, datesWhere)
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()
if not row.RoleModifier then
-- cast false as nil
row.RoleModifier = nil
end
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'
for _, key in ipairs({ subArgName, 'sub', 'Sub' }) do
if args[key] and util_args.castAsBool(args[key]) then
return 'Sub'
end
end
for _, key in ipairs({ traineeArgName, 'trainee', 'Trainee' }) do
if args[key] and util_args.castAsBool(args[key]) then
return 'Trainee'
end
end
return nil
end
return p