Documentation for this module may be created at Module:TeamMembers/doc
local util_args = require('Module:ArgsUtil')
local util_cargo = require("Module:CargoUtil")
local util_esports = require("Module:EsportsUtil")
local util_data = require("Module:DataUtil")
local util_form = require("Module:FormUtil")
local util_html = require("Module:HtmlUtil")
local util_infobox = require("Module:InfoboxUtil")
local util_map = require("Module:MapUtil")
local util_news = require("Module:NewsUtil")
local util_sort = require("Module:SortUtil")
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_title = require("Module:TitleUtil")
local util_toggle = require("Module:ToggleUtil")
local util_vars = require("Module:VarsUtil")
local i18n = require('Module:i18nUtil')
local m_team = require('Module:Team')
local Region = require('Module:Region')
local RoleList = require('Module:RoleList')
local DEBUG = false
local ARGS = { 'Player', 'Country', 'Residency', 'IrlName', 'Role', 'DateJoin', 'DateLeave', 'ContractEnd', 'SortKey' }
local SETTINGS = require('Module:TeamMembers/Settings')
local TOGGLES_DATE = {
order = { 'approx', 'exact' },
key = 'date'
}
local h = {}
local p = {}
function p.fromCargo(frame)
i18n.init('TeamMembers', 'NewsUtil')
local args = util_args.merge()
h.castArgs(args)
h.setConstants(args)
h.setColumns()
util_toggle.oflInit(TOGGLES_DATE)
local listOfChanges = h.queryForChanges(args.team or mw.title.getCurrentTitle().text, args)
h.processChangesRows(listOfChanges)
local players = h.computePlayers(listOfChanges)
return h.getPlayerInfoAndMakeOutput(players, args)
end
function p.fromArgs(frame, args)
i18n.init('TeamMembers')
local args = util_args.merge()
h.castArgs(args)
h.setConstants(args)
h.setColumns()
util_toggle.oflInit(TOGGLES_DATE)
local players = h.getPlayersFromArgs(args)
return h.getPlayerInfoAndMakeOutput(players)
end
-- private
function h.castArgs(args)
args.team = args.team and m_team.teamlinkname(args.team)
args.debug = util_args.castAsBool(args.debug)
if not args.when then args.when = 'current' end
end
function h.setConstants(args)
DEBUG = args.debug
if not SETTINGS[args.when] then
error(i18n.print('error_InvalidWhen'))
end
SETTINGS = SETTINGS[args.when]
end
function h.setColumns()
if DEBUG then SETTINGS.columns[#SETTINGS.columns+1] = '_pageName' end
end
-- from cargo
function h.queryForChanges(team, args)
return util_cargo.queryAndCast(h.getChangesQuery(team, args))
end
function h.getChangesQuery(team, args)
local query = {
tables = {
'TeamRedirects=TR',
'TenuresUnbroken=Tenures',
'TenuresUnbroken__RosterChangeIds=RCID',
'RosterChanges=RC',
'NewsItems=News',
-- this PR is for a field, not a join
'PlayerRedirects=PR',
},
join = {
'TR.AllName=Tenures.Team',
'Tenures._ID=RCID._rowID',
'RCID._value=RC.RosterChangeId',
'RC.NewsId=News.NewsId',
'Tenures.Player=PR.AllName',
},
where = h.getChangesWhere(team, args),
fields = h.getChangesFields(),
oneToMany = h.getChangesOneToMany(),
orderBy = 'PR.AllName, News.Date_Sort ASC, TenuresPrimaryKey ASC',
}
return query
end
function h.getChangesWhere(team, args)
local tbl = {
('TR._pageName="%s"'):format(team),
SETTINGS.where,
}
-- ugly workaround for https://phabricator.wikimedia.org/T232190
if team == "Storm Teams" then
tbl[1] = ('TR._pageName=CONCAT("Storm T", "eams")')
end
return util_cargo.concatWhere(tbl)
end
function h.getChangesFields()
local ret = {
-- PlayerKey is needed here so we have a standardized way of matching up with
-- PR.AllName in the playerExtraInfo field later on
-- this is due to Cargo treating modified unicode letters the same as
-- their non-unicode varieties
'PR.AllName=PlayerKey',
'Tenures.Player',
'Tenures.NameLeave=Name',
'Tenures._ID=TenuresPrimaryKey',
'Tenures.DateJoin=DateJoin',
'Tenures.DateLeave=DateLeave',
'Tenures.ContractEnd',
'Tenures.ResidencyLeave=Residency [region]',
'Tenures.NameLeave=Name',
'Tenures.NextTeam',
'Tenures.NextIsRetired [boolean]',
'Tenures.NextIsWildrift [boolean]',
'Tenures.NextIsDeceased [boolean]',
'Tenures.IsCurrent [boolean]',
}
return ret
end
function h.getChangesOneToMany()
local oneToMany = {
groupBy = { 'TenuresPrimaryKey' },
fields = {
RosterChanges = {
'RC.RoleModifier',
'RC.RolesIngame__full=RolesIngame',
'RC.Roles__full=Roles',
'RC.Status',
'RC.Direction',
'RC.NewsId', -- for debugging
'News.Date_Display',
'News.Date_Sort',
'News.IsApproxDate [boolean]',
'News.Source',
'News.Sentence',
}
}
}
return oneToMany
end
function h.processChangesRows(listOfChanges)
util_map.rowsInPlace(listOfChanges, h.processOneChangeRow)
end
function h.processOneChangeRow(row)
h.moveRosterChangesToExpectedNames(row)
-- only completely unambiguous constants should be handled here
-- anything that can change before/after needs to be gotten from the LAST roster change
-- in the function below this
row.SortKeyName = row.Name:lower()
row.SortKeyLeave = row.DateLeave
row.DateDisplayJoin = h.getDateDisplay(row, 'Join')
row.DateDisplayLeave = h.getDateDisplay(row, 'Leave')
end
function h.moveRosterChangesToExpectedNames(row)
if not row.RosterChanges or not row.RosterChanges[1] then return end
if row.RosterChanges[1].Direction == 'Join' then
row.Date_DisplayJoin = row.RosterChanges[1].Date_Display
row.Date_SortJoin = row.RosterChanges[1].Date_Sort
row.IsApproxDateJoin = row.RosterChanges[1].IsApproxDate
row.SourceJoin = row.RosterChanges[1].Source
row.SentenceJoin = row.RosterChanges[1].Sentence
end
local last = row.RosterChanges[#row.RosterChanges]
row.RoleModifier = last.RoleModifier
row.Status = last.Status
-- we could change this to be a RoleList, or we can just listen to the ingame/all
-- params that we got from db
-- either way is fine, and i don't see any inherent advantage to one over the other
row.RolesIngame = RoleList(last.RolesIngame, { modifier = row.RoleModifier })
row.Roles = RoleList(last.Roles, { modifier = row.RoleModifier })
if row.Roles:hasIngame() then
row.SortKeyRole = row.RolesIngame:sortnumber()
else
row.SortKeyRole = row.Roles:sortnumber()
end
if last.Direction == 'Leave' then
row.Date_DisplayLeave = last.Date_Display
row.Date_SortLeave = last.Date_Sort
row.IsApproxDateLeave = last.IsApproxDate
row.SourceLeave = last.Source
row.SentenceLeave = last.Sentence
end
end
function h.getDateDisplay(row, when)
return ('%s<span class="team-members-ref">%s</span>'):format(
util_news.getDateDisplayForTable(row, when) or '',
util_news.getSentenceAndRefDisplay(row, when)
)
end
function h.getRefSentencePopup(row, when)
local popup = util_toggle.popupButton()
popup.inner:wikitext(row['Sentence' .. when])
:addClass('team-members-sentence')
popup.wrapper:addClass('team-members-sentence-wrapper')
popup.span:addClass('team-members-sentence-span')
return tostring(popup.span)
end
function h.computePlayers(listOfChanges)
local players = { keys = {} }
for _, row in ipairs(listOfChanges) do
if h.isAPlayer(row) then
util_table.push(players.keys, row.Player)
if not row.PlayerKey then
error(i18n.print('error_MissingRedirects', row.Player))
end
row.PlayerKey = mw.ustring.lower(row.PlayerKey)
players[#players+1] = row
end
end
return players
end
function h.isAPlayer(row)
-- this could end up more complex later
if row.Status == 'official_sub' or row.RolesIngame:exists() then
return true
end
end
-- from args
function h.getPlayersFromArgs(args)
local arr = util_args.splitArgsArray(args.members, ARGS)
local players = { keys = {} }
for _, row in ipairs(arr) do
local key = util_title.escape(mw.ustring.lower(row.Player))
util_table.push(players.keys, key)
row.PlayerKey = key
players[#players+1] = row
end
return players
end
-- merge from cargo and from args
function h.getPlayerInfoAndMakeOutput(players, args)
if not next(players.keys) then return h.makeNoResultsOutput() end
local playerExtraInfo = h.queryForPlayerExtraInfo(players)
util_map.unorderedDictRowsInPlace(playerExtraInfo, h.formatOnePlayerExtraInfo)
util_map.rowsInPlace(players, h.formatOnePlayer)
h.addInfoToPlayers(players, playerExtraInfo)
util_map.rowsInPlace(players, h.formatOneFinalPlayer)
util_sort.tablesByKeys(players, SETTINGS.sort_fields, SETTINGS.sort_ascending)
util_data.removeUnusedColumns(SETTINGS.columns, players)
return h.makeNotice(args), h.makeOutput(players)
end
function h.makeNoResultsOutput()
if util_vars.getBool('isdisbanded') then return '<!-- -->' end
return i18n.print('noResultsText')
end
-- Output
function h.queryForPlayerExtraInfo(players)
local dict = util_cargo.getRowDict(h.getPlayerExtraInfoQuery(players), 'PlayerKey')
return h.normalizePlayerExtraInfo(dict)
end
function h.getPlayerExtraInfoQuery(players)
local query = {
tables = {
'PlayerRedirects=PR',
'Players=P',
'Alphabets=A',
},
join = {
'PR._pageName=P._pageName',
'P.NameAlphabet=A.Alphabet',
},
fields = h.getPlayerExtraInfoFields(),
where = h.getPlayerExtraInfoWhere(players),
}
return query
end
function h.getPlayerExtraInfoFields()
local ret = {
-- matches the PlayerKey from earlier
'PR.AllName=PlayerKey',
'COALESCE(P.NationalityPrimary,P.Country)=Country [country]',
'P.Name=IrlName',
'P.NativeName',
'P.Residency [region]',
'P.Team=CurrentTeam',
'P._pageName=PlayerPage',
'A.IsTransliterated[boolean]'
}
return ret
end
function h.getPlayerExtraInfoWhere(players)
return util_cargo.concatWhereOr(
util_map.format(
players.keys,
'PR.AllName="%s"'
)
)
end
function h.formatOnePlayerExtraInfo(row)
row.Contract = row.Contract or '-'
end
function h.formatOnePlayer(row)
row.PlayerDisplay = util_esports.playerLinked(row.Name)
end
function h.normalizePlayerExtraInfo(dict)
local normalized = {}
for k, v in pairs(dict) do
normalized[mw.ustring.lower(k)] = v
end
return normalized
end
-- add info to players
function h.addInfoToPlayers(players, playerExtraInfo)
-- PlayerKey in both cases is from PlayerRedirects
-- so we'll guarantee in both cases get the PR version of the name
-- we also lowercased it earlier, but that shouldn't actually matter
for _, playerData in ipairs(players) do
-- if not playerExtraInfo[playerData.PlayerKey] then
-- util_vars.log(playerData.PlayerKey)
-- end
util_table.mergeDontOverwrite(playerData, playerExtraInfo[playerData.PlayerKey])
end
end
function h.formatOneFinalPlayer(row)
row.classes = {}
row.Country = row.Country:image()
row.Residency = row.Residency:image()
row.RoleDisplay = h.getRoleDisplay(row)
row.IrlNameDisplay = h.getIrlNameDisplay(row)
h.addNextTeamDisplay(row)
local teamHistPopup = h.getTeamHistPopup(row.Player)
row.NextTeamDisplay = row.NextTeamDisplay .. teamHistPopup
row.DateDisplayJoinWithPopup = h.concatDateDisplayAndPopup(row.DateDisplayJoin, teamHistPopup)
if row.ContractEnd and not util_time.dateIsInFuture(row.ContractEnd) then
row.classes.ContractEnd = 'team-members-expired'
end
row.attrs = {
PlayerDisplay = {
['data-player-id'] = row.Name,
},
RoleDisplay = {
['data-sort-value'] = row.SortKeyRole,
},
DateDisplayJoinWithPopup = {
['data-sort-value'] = util_time.unix(row.Date_SortJoin)
},
DateDisplayJoin = {
['data-sort-value'] = util_time.unix(row.Date_SortJoin)
},
DateDisplayLeave = {
['data-sort-value'] = util_time.unix(row.Date_SortLeave)
},
}
row.classes.DateDisplayJoinWithPopup = 'team-members-date-with-popup'
end
function h.getRoleDisplay(row)
local role = row.RolesIngame:flairs{len='role'} or row.Roles:flairs{len='role'}
if not row.Status then return role end
local toggle = util_toggle.allToggleAll(nil, 'member-statuses')
toggle.button:wikitext('(+) ')
toggle.button2:wikitext('(–) ')
toggle.content:wikitext((' %s'):format(i18n.print(row.Status)))
return role .. ' ' .. tostring(toggle.tbl)
end
function h.getIrlNameDisplay(row)
if not row.NativeName then return row.IrlName end
if not row.IrlName then return nil end
if not row.IsTransliterated then return row.IrlName end
return ('%s (%s)'):format(row.IrlName, row.NativeName)
end
function h.addNextTeamDisplay(row)
if row.NextIsDeceased then
row.NextTeamDisplay = m_team.rightshort('deceased')
return
end
if row.NextIsWildrift then
row.NextTeamDisplay = m_team.rightshort('wild rift')
return
end
if row.NextIsRetired then
row.NextTeamDisplay = m_team.rightshort('retired')
return
end
row.NextTeam = row.NextTeam or row.CurrentTeam
if row.NextTeam then
row.NextTeamDisplay = m_team.rightshortlinked(row.NextTeam)
return
end
row.classes.NextTeamDisplay = 'newteam-none'
row.NextTeamDisplay = 'None'
end
function h.getTeamHistPopup(player)
local button = util_toggle.popupButtonLazy(
nil,
'tmtimeline',
('PlayerTeamHistoryPopup|%s'):format(player)
)
return tostring(button.span)
end
--
function h.concatDateDisplayAndPopup(date, popup)
local output = mw.html.create()
output:tag('div')
:addClass('team-members-date-container')
:wikitext(date)
output:wikitext(popup)
return tostring(output)
end
-- output
function h.makeNotice(args)
if args.when ~= 'current' then return '' end
return mw.getCurrentFrame():expandTemplate{title = 'ContractExpirationNotice' }
end
function h.makeOutput(players)
local output = mw.html.create()
util_infobox.printLowContentNoticeIfNeeded(output, i18n.print('lowContentNotice'))
h.printToggler(output)
local tbl = output:tag('table')
:addClass('wikitable')
:addClass('sortable')
:addClass('team-members')
:addClass('hoverable-rows')
:addClass(SETTINGS.parent_class)
util_html.printHeaderFromI18n(tbl, SETTINGS.columns)
h.printRows(tbl, players)
return output
end
function h.printToggler(output)
local div = output:tag('div')
:addClass('toggle-button')
div:wikitext(i18n.print('toggleDatesIntro'))
util_toggle.printOptionFromListTogglers(div, TOGGLES_DATE)
end
function h.printRows(tbl, players)
for _, row in ipairs(players) do
local tr = util_html.printRowByList(tbl, row, SETTINGS.columns)
h.printRefreshButton(util_html.lastChild(tr), row.PlayerPage)
end
end
function h.printRefreshButton(td, player)
td:addClass('lastcell')
local div = td:tag('div')
:addClass('lastcell-action-pretty')
:addClass('team-members-refresh')
:attr('data-player', player)
end
return p