Module documentation

This module is used for creating a staff listing which allows all or parts of the staff to be displayed in a variety of formats. The contents are stored in a template's staff parameter in a JSON format, including data on all individual departments and employees within the list.

Individual departments (or teams, or subteams, or other similar groups which would be marked in some way) are stored as JSON objects with the following parameters:

  • "name": stores the name of the group. The value should be wrapped in translate tags so that it can be displayed in the appropriate language.
  • "id": stores a stable identifier for the group, so that the template can request data on it.
  • "notTeam": should be marked as true if the group is not a team which should be distinctly listed in lists of teams in, for example, the navbox of all departments and teams.
  • "head": should point to the employee who is the head of the group.
  • "page": should be the title of the page on Meta-wiki (or other Wikimedia wiki, with the appropriate interwiki prefix) which gives information about the group.
  • "members": stores an array of all employees which are directly within the group. (Employees that are part of a subsidiary team within the group should not be listed in the members array.)
  • "subs": stores an array of all groups that are within the group. These groups should be formatted in the same way as the higher-level departments.

Individual employees are stored as objects with the following parameters:

  • "name": stores the name of the employee.
  • "username": stores the on-wiki username of the employee, omitting the "User:" namespace prefix.
  • "homewiki":, for employees whose home wiki must be specified as not being on Meta-wiki, stores the interwiki prefix of the employee's home wiki. (Example: "mw" for mediawiki.org.)
  • "title": stores a the name of the employee's position
  • "titleId": stores a stable identifier for the position, so that the template can request data on the holder of the position.
  • "isContractor": should be marked true if the employee is a contractor.
  • "isMascot": should be marked true if the "employee" is actually just a mascot which should not be counted in employee counts.
  • "image": stores the name of an image on Commons of the employee, omitting the "File:" namespace prefix.

All of these parameters are optional except for name. Further information on template usage can be found on Template:WMF Staff/doc.

local defaultImg = "Wikimedia Foundation office camera shy.png"
local Titlelib = require('Module:Titlelib')

function mapjoin( obj, fn, separator )
  local s = ""
  if ( obj ) then
    for k, v in pairs( obj ) do
      local r = fn( v, k )
      if r and r ~= "" then
      	if separator and s ~= "" then
      	  s = s .. separator
      	end
        s = s .. r
      end
    end
  end
  return s
end

-- Decode the JSON string passed as template parameter. Escape the quotation marks
-- in Extension:Translate’s “outdated translation” and “missing translation”
-- marker before decoding to ensure the validity of the JSON string.
-- (The extension uses a <div> instead of a <span> when the translation unit
-- consists of multiple lines, but hopefully such input will never appear here,
-- as raw linebreaks in strings aren’t valid JSON in the first place.)
local function decode_translated_json( text )
  text = string.gsub( text, '<span class="mw%-translate%-fuzzy">', '<span class=\\"mw-translate-fuzzy\\">' )
  text = string.gsub( text, '<span lang="en" dir="ltr" class="mw%-content%-ltr">', '<span lang=\\"en\\" dir=\\"ltr\\" class=\\"mw-content-ltr\\">' )
  return mw.text.jsonDecode( text )
end

-- Return a function which return true if this user hasn't already been run
-- through that function.
function startNoDup()
  local cache = {}
  return function ( obj )
    if not obj.username then return true end
    if not cache[ obj.username ] then
      cache[ obj.username ] = true
      return true
    end
  end
end

function link_user( user )
  return ( user.username and
    "[[" .. ( user.homewiki and user.homewiki .. ":" or "" ) .. "User:" .. user.username .. "|" .. user.name .. "]]" or
    user.name
  )
end

function section_link( dep )
  return dep.page and "[[" .. Titlelib.myLangLink(dep.page) .. "|" .. dep.name .. "]]" or dep.name
end

function title_link( user, suppress_contractor_label )
  local contractor = user.isContractor and ( not suppress_contractor_label ) and
    " " .. ( mw.getCurrentFrame().args.contractor or "(Contractor)" ) or
    ""
  return ( user.titleLink and "[[" .. user.titleLink .. "|" .. user.title .. "]]" or user.title ) .. contractor
end

-- Show a gallery entry for a single user
function gallery_entry( user )
  if user then
    local img = "File:" .. ( user.image ~= "" and user.image or defaultImg )
    local userLink = "'''" .. link_user( user ) .. "'''"
    local titleLink = title_link( user )
    return img .. '|class=notpageimage|' .. userLink .. "<br /><small>" .. titleLink .. "</small>\n"
  else
    return ""
  end
end

-- Gallery of entries for a list of users
function gallery_group( group, noDup )
  return mapjoin( group, function ( v )
    return ( not noDup or noDup( v ) ) and gallery_entry( v )
  end )
end

-- Show a gallery group of all the users in a department/team/subteam.
function chunk_gallery( dep, size, noDup )
  local s = ""
  noDup = noDup or startNoDup()
  if dep.head and noDup( dep.head ) then s = s .. gallery_entry( dep.head ) end
  s = s .. gallery_group( dep.members, noDup )
  -- s = s .. mapjoin( dep.subs, chunk_gallery )
  s = s .. mapjoin( dep.subs, function ( v ) return chunk_gallery( v, size, noDup ) end )
  return s
end

function gallery_tag( frame, contents, settings )
  -- For testing
  --return "<gallery " .. mapjoin( settings, function ( v, k ) return k .. '="' .. v .. '"' end, " " ) .. "'>" ..
  --  contents .. "</gallery>"
  return frame:extensionTag( "gallery", contents, settings )
end

-- Format a single department into a gallery, with headers for each team.
function department_gallery( frame, dep, size, hLevel, useSubheaders )
  size = size and size ~= "" and tonumber( size ) or 200
  
  local s = gallery_entry( dep.head ) .. gallery_group( dep.members )
  
  local gallery_settings = { mode = "nolines",
    widths = size .. "px",
    -- Normal ratio is 3 / 2, but also allow 10% horizontal margin.
    heights = ( math.floor( ( size * 0.9 ) / 3 * 2 ) ) .. "px"
  }
  
  if s ~= "" then
    s = gallery_tag( frame, s, gallery_settings ) .. "\n"
  end
  
  s = s .. mapjoin( dep.subs, function ( v )
    return "<h" .. hLevel .. ">" .. section_link( v ) .. "</h" .. hLevel .. ">\n" ..
      (
        useSubheaders and
          department_gallery( frame, v, size, hLevel + 1, true )
            or
          gallery_tag( frame, chunk_gallery( v ), gallery_settings ) .. "\n"
      )
  end )
  
  return s
end

-- Show the rows of a table containing each of the top-level department heads,
-- plus those holding certain specified roles (titleIds)
function leadership_list( staff, titleIds, withHeads )
  function create_row( dep, user )
    return "\n|-" ..
      "\n|<div style='max-height: 150px; overflow: hidden;'>[[File:" .. ( user.image ~= "" and user.image or defaultImg ) .. "|220px|frameless|class=notpageimage]]</div>" ..
      "\n|" .. link_user( user ) ..
      "\n|" .. title_link( user, true ) ..
      ( withHeads and "\n|" .. section_link( dep ) or "" )
  end
  
  withHeads = withHeads and withHeads ~= "false"
  
  return mapjoin( staff, function ( dep )
    local r = ""
    if withHeads and dep.head then
      r = create_row( dep, dep.head )
    end
    
    foreach_title_id( dep, titleIds, function ( found )
      r = r .. create_row( dep, found )
    end )
    
    return r
  end )

end

-- Show a list of users
function group_list( group, indent )
  return mapjoin( group, function( v )
    return indent .. link_user( v ) .. "\n"
  end )
end

-- List all members of the team/department
function section_list( dep, boldHead, indent )
  local s = ""
  indent = indent or "*"

  if dep.head then
    s = s .. indent .. ( boldHead and "'''" .. link_user( dep.head ) .. "'''" or link_user( dep.head ) ) .. "\n"
  end
  if dep.members then s = s .. group_list( dep.members, indent ) end
  
  s = s .. mapjoin( dep.subs, function ( v )
    return indent ..
      section_link( v ) .. "\n" ..
      section_list( v, false, indent .. "*" )
  end )
  
  return s
end

function create_navbox( frame, params, obj, fn )
  params.bodyclass = "hlist"
  -- Allow overriding navbox classes
  for k, v in pairs( { "title", "above", "below", "belowclass", "name", "state", "background" } ) do
    if frame.args[ v ] and frame.args[ v ] ~= "" then
      params[ v ] = frame.args[ v ]
    end
  end
  
  if obj then
    for k, v in pairs( obj ) do
      local r = fn( v, k )
      params[ "group" .. k ] = r[ 1 ]
      params[ "list" .. k ] = r[ 2 ]
    end
  end
  
  return frame:expandTemplate{ title = "Navbox department", args = params }
end

-- Create a navbox with all teams listed and their members
function all_teams_navbox( dep, frame, extraTLRoles )
  local above = ""
  local noDup = startNoDup()
  
  if dep.head and noDup( dep.head ) then
    above = title_link( dep.head ) .. ": '''" .. link_user( dep.head ) .. "'''"
  end
  
  foreach_title_id( dep, extraTLRoles, function ( found )
  	if noDup( found ) then
      above = above .. "<br />" .. title_link( found ) .. ": '''" .. link_user( found ) .. "'''"
    end
  end )
  
  if dep.members then
  	local membersList = mapjoin( dep.members, function ( v, i )
      if noDup( v ) and not v.isMascot then
        return link_user( v )
      end
    end, " &bull; " )
    if membersList and membersList ~= "" then
      above = above .. "<br />(" ..  membersList .. ")"
    end
  end
  return create_navbox( frame, {
    name = "",
    title = section_link( dep ),
    above = above
  }, dep.subs, function ( v )
    return { section_link( v ), section_list( v, true ) }
  end )
end

function staff_count( staff, noDup )
  local staffCount, contCount = 0, 0
  noDup = noDup or startNoDup()
  
  local count_person = function ( x )
    if x then
      if noDup( x ) and not x.isMascot then
        if x.isContractor then
          contCount = contCount + 1
        else
          staffCount = staffCount + 1
        end
      end
    end
  end
  
  for k, v in pairs( staff ) do
    if v.head then
      count_person( v.head )
    end
    
    if v.members then
      for kk, vv in pairs( v.members ) do
        count_person( vv )
      end
    end
    if v.subs then
      local subStaff, subCont = staff_count( v.subs, noDup )
      staffCount, contCount = staffCount + subStaff, contCount + subCont
    end
  end
  return staffCount, contCount
end

function foreach_title_id( dep, titleIds, fn )
  local titleIdSet = titleIds and titleIds ~= "" and mw.text.split( titleIds, "," )
  if titleIdSet then
    for k, id in pairs( titleIdSet ) do
      local found = find_by_title_id( dep, id )
      if found then
        fn( found )
      end
    end
  end
end

-- WIP
function find_by_title_id( section, id )
  if section.head and section.head.titleId == id then
    return v
  end
  if section.members then
    for k, v in pairs( section.members ) do
      if v.titleId and v.titleId == id then
        return v
      end
    end
  end
  if section.subs then
    for k, v in pairs( section.subs ) do
      local found = find_by_title_id( v, id )
      if found then
        return found
      end
    end
  end
end

function find_by_id( staff, id )
  for k, v in pairs( staff ) do
    if v.id == id then
      return v
    elseif v.subs then
      local found = find_by_id( v.subs, id )
      if found then
        return found
      end
    end
  end
end

-- List subdivisions (not individual staffers)
function list_subs( dep, indent )
  local indent = indent or "*"
  return mapjoin( dep.subs, function ( team )
    if not team.notTeam then
      return indent .. section_link( team ) .. "\n" .. list_subs( team, indent .. "*" )
    end
  end )
end

-- Create "Template:Wikimedia Foundation departments" navbox
function all_departments_navbox( deps, frame )
  return create_navbox( frame, {
    name = ""
  }, deps, function ( v, i )
--    if i == 1 then
      -- Hardcode ED office row.
--      return {
--        section_link( { page = v.head.titleLink, name = ( frame.args.navbox_execbox or "Chief Executive Officer<br />and Executive Director" ) } ),
--        "* " .. link_user( v.head ) ..
--        "\n* [[Special:MyLanguage/" ..
--        ( frame.args.navbox_exec_link or "Wikimedia Foundation Leadership team" ) ..
--        "|" .. ( frame.args.navbox_exec_leadership or "Leadership" ) .. "]]"
--      }
--    else
      local list = list_subs( v )
      return { section_link( v ), ( list ~= "" and list or ( frame.args.noteams or "''No teams''" ) ) }
--    end
  end )
end

function show_all_staff( frame, staff, arg1, arg2 )
  local useSubheaders = arg1 and arg1 ~= "" and arg1 ~= "false"
  local size = arg2 and arg2 ~= "" and arg2 or 200
  return mapjoin( staff, function( dep )
    return "<h2>" .. section_link( dep ) .. "</h2>" .. department_gallery( frame, dep, size, 3, useSubheaders ) .. "\n"
  end )
end

-- Alt style based on old staff list from foundation.wikimedia wiki
-- This might be removed at some point.
function show_all_staff_alt1( frame, staff, arg1 )
  return mapjoin( staff, function( dep )
  	local largeSize = 165
    local smallSize = 140
  	
    local headblock = [[<div style="float:left;padding-top: 21px; margin-bottom:40px; max-width: 190px;">]] ..
      ( dep.head and gallery_tag( frame, gallery_entry( dep.head ), { mode = "nolines", widths = largeSize .. "px" } ) or "" ) ..
      "</div>"
    
    local s = gallery_group( dep.members )
    
    if s ~= "" then
      s = gallery_tag( frame, s, { mode = "nolines", widths = smallSize .. "px" } ) .. ( dep.subs and "<hr />" or "" ) .. "\n"
    end
    
    s = s .. mapjoin( dep.subs, function ( v )
      return "<h3>" .. section_link( v ) .. "</h3>\n" ..
        department_gallery( frame, v, smallSize, 4, true )
    end, "<hr />" )
    
    return "<h2 style='clear: left;'>" .. section_link( dep ) .. "</h2>" ..
      headblock ..
      [[<div style="margin-left: 200px; padding-top: 14px; color:#5E5E5E;"><p><br /></p>]] ..
        s ..
      "</div>" ..
      "\n"
    
  end )
end

return {
  g = function( json ) -- For testing
    local staff = decode_translated_json( json )
    --return all_departments_navbox( staff, mw.getCurrentFrame() )
    --return all_teams_navbox(
    --  find_by_id( staff, 'communityengagement' ),
    --  mw.getCurrentFrame(),
    --  'vpsupport'
    --)
    
    --return department_gallery( mw.getCurrentFrame(), find_by_id( staff, 'contributors' ), 200, 3 )
    return show_all_staff( mw.getCurrentFrame(), staff, "", 225 )
    --return show_all_staff_alt1( mw.getCurrentFrame(), staff )
    --return leadership_list( staff, "vpsupport", false )
    --return section_list( find_by_id( staff, 'communications' ), true )
    --return staff_count( staff )
    --return staff_count( { find_by_id( staff, 'contributors' ) } )
  end, -- For testing
  
  main = function ( frame )
    local args = frame.args
    local staff = decode_translated_json( args.staff )
    mw.logObject( staff )
    local type = args.type
    local section = args.section
    
    if section and section ~= "" then
      section = find_by_id( staff, section )
      if not section then
        -- Error: Section requested, but not found.
        return ""
      end
    else
      section = staff
    end
    
    if type == "showall" then
      return show_all_staff( frame, staff, args[ 1 ], args[ 2 ] )
    elseif type == "department_gallery" then
      -- Show gallery of single department
      return department_gallery( frame, section, args[ 1 ], 3 )
    elseif type == "team_gallery" then
      return department_gallery( frame, section, args[ 1 ], 3 )
    elseif type == "departments_navbox" then
      return all_departments_navbox( staff, frame )
    elseif type == "teams_navbox" then
      -- Show navbox of all teams in a department, with staff
      return all_teams_navbox( section, frame, args[ 1 ] )
    elseif type == "team_list" then
      return section_list( section, true )
    elseif type == "staff_count" then
      local staffCount, contCount = staff_count( section )
      local r = string.gsub( args[ 1 ], "$[123]", function ( n ) return ({
        ["$1"] = staffCount + contCount,
        ["$2"] = staffCount,
        ["$3"] = contCount
      })[ n ] or n end )
      return r
    elseif type == "leadership_list" then
      return leadership_list( staff, args[ 1 ], args[ 2 ] )
    elseif type == "showall_alt1" then
      return show_all_staff_alt1( frame, staff, args[ 1 ] )
    else
      return error( "Staff list - invalid type" )
    end
  end
}