Jump to content

Module:Probox

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Jmorgan (WMF) (talk | contribs) at 20:29, 25 July 2014 (convert args to lowercase before checking categories). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

--
-- This module implements {{Probox}}
--
local getArgs = require('Module:Arguments').getArgs
local TableTools = require('Module:TableTools')
local p = {}

function addCategories(data, args, open_roles)
	-- will also look for numbered categories
	local cat_list = {}
	local base_cat = data.categories.base
	-- table.insert(cat_list, base_cat) --always need a base category
	-- -- adding role categories
	if data.categories.default then
		table.insert(cat_list, data.categories.default)
	end	
	if data.categories.roles then
		role_cat = data.categories.roles
		for k,v in pairs(open_roles) do
			table.insert(cat_list, base_cat .. k:gsub("^%l", string.upper) .. role_cat)
		end
	end
 	for k,v in pairs(data.categories) do -- if there is a matching arg, and it has a table of category possibilities
 		if (args[k] and string.len(args[k]) > 0 and type(data.categories[k]) == "table") then --convert args to lowercase before checking them against cats
			for cat_key, cat_val in pairs(data.categories[k]) do
				if stringToLowerCase(args[k]) == cat_key then --convert args to lowercase before checking them against cats
					table.insert(cat_list, base_cat .. cat_val)
					break
				end
			end	
		end
 	end	
 	local page_categories = " [[" .. tostring(table.concat(cat_list, "]] [[")) .. "]]"
	return page_categories
end		

function makeTextField(field, field_div)
	-- makes a formatted text field for output, based on parameter
	-- values or on default values provided for that field 
	-- type in the stylesheet
	field_div:cssText(field.style)
	if field.vtype2 == "body" then
		field_div
			:tag('span')
				:cssText(field.style2)--inconsistent use of styles 2/3 here
				:wikitext(field.title)--we probably aren't using this for most things now, after Heather's redesign	
				:done()
			:tag('span')
				:cssText(field.style3)
				:wikitext(field.values[1])
				:done()							
	elseif field.vtype2 == "title" then
		field_div
			:tag('span')
				:cssText(field.style3)
				:wikitext(field.values[1])
				:done()			
	elseif field.vtype2 == "link" then
		if field.arg == "toolkit" then --more special casing for IdeaLab!
			field_div
			:tag('span')
				:cssText(field.style3)	
				:wikitext("[[" .. field.icon .. "|" .. field.alignment .. "|" .. field.width .."|link=" .."]] " .. "[[" .. field.values[1] .. "|<span style='" .. field.style2 .. "'>" .. field.title .."</span>]]")
				:done()	
		else		
			field_div
				:tag('span')
					:cssText(field.style3)	
					:wikitext("[[" .. field.values[1] .. "|<span style='" .. field.style2 .. "'>" .. field.title .."</span>]]")
					:done()
		end			
	end
	field_div:done()
	return field_div
end						

function makeImageField(field, field_div)
	-- makes a formatted image field for output, based on parameter values
	-- provided in the calling template or its parent template, or on default
	-- values provided in the stylesheet
	field_div:cssText(field.style)
	field_div
		:tag('span')
		:cssText(field.style3)
	if field.vtype2 == "thumb" then	
		field_div					
			:wikitext("[[" .. field.values[1] .. "|right|"  .. field.width .."]]")
	elseif field.vtype2 == "link" then
		field_div
			:wikitext("[[" .. field.values[1] .. "|" .. field.alignment .. "|" .. field.width .."|link=" .. field.link .. "]]")	
	elseif field.vtype2 == "ui_button" then
		field_div
			:addClass(field.class)			
			:wikitext(field.title)
			:done()	
	end		
	field_div:done()				
	return field_div		
end	

function makeParticipantField(field, ftype)
	local field_div = mw.html.create('div')
	local title_span
	if field.icon then 
		title_span = "[[" .. field.icon .. "|left" .. "|18px]] " .. field.title
	else
		title_span = field.title
	end	
	field_div
		:cssText(field.style)
		:tag('span')
			:cssText(field.style2)
			:wikitext(title_span)
			:done()
	if ftype == "filled" then
		local i = 1
		for k,v in ipairs(field.values) do
			if (i > 1 and field.icon) then --only insert extra padding if has icon
				field.style3 = "padding-left:25px; display:block"
			end	
			field_div
				:tag('span')
					:cssText(field.style3)
					:wikitext("• " .. "[[User:" .. v .. "|" .. v .. "]]")
					:done()	
			i = i + 1
		end	
	end	
	field_div:allDone()
	return field_div
end

function makeSectionDiv(sec_fields, sec_style)
	local sec_div = mw.html.create('div'):cssText(sec_style)	
	sec_fields = TableTools.compressSparseArray(sec_fields)
	for findex, sec_field in ipairs(sec_fields) do -- should put this at the end of the function, and just append the other stuff
		sec_div:node(sec_field)
	end	
	return sec_div
end

function makeParticipantsSection(frame, args, data, filled_role_data, open_roles)
	local filled_role_fields = {}
	for role, val_table in pairs(filled_role_data) do
		local field = data.fields[role]
		field.title = mw.text.trim(frame:expandTemplate{title=args.translations, args={field.key}})
		field.values = {}			
		for val_num, val_text in ipairs(filled_role_data[role]) do
			field.values[#field.values + 1] = val_text		
		end
		local filled_field_div = makeParticipantField(field, "filled")	
        filled_role_fields[field.rank] = filled_field_div	
	end
	local sec_div = makeSectionDiv(filled_role_fields, data.styles.section["participants"]) 
	if args.more_participants == "YES" then -- really need this here?
		if args.portal == "Idealab" then -- beware, exceptions everywhere
			sec_div:tag('span'):cssText("font-style:italic; color: #888888"):wikitext("this project could really use..."):done()
		elseif args.portal == "Patterns" then	
			sec_div:tag('span'):cssText("font-style:italic; color: #888888"):wikitext("a learning pattern for..."):done()
		else			
		end	
		for role, val in pairs(open_roles) do -- should make these ordered using compressSparseArray, as above
			local field = data.fields[role]
			field.title = mw.text.trim(frame:expandTemplate{title=args.translations, args={field.key}})
			if field.icon then
				field.icon = field.icon_inactive
			end	
			local open_field_div = makeParticipantField(field, "open")		
			sec_div:node(open_field_div)
		end
	end	
	sec_div:allDone()
	return sec_div
end		

function makeSectionFields(args, field)
	-- ui button is separate
	local field_div = mw.html.create('div'):cssText(field.style) --why declare this here?
	if field.vtype == "image" then
		if (field.isRequired == true or (args[field.arg] and string.len(args[field.arg]) > 0))  then --should move this up, may not just apply to images
			field_div = makeImageField(field, field_div)
		end	
	elseif field.vtype == "text" then
		field_div = makeTextField(field, field_div)	
	else
	end	
	return field_div -- make sure div is 'done'
end

function makeSection(frame, args, data, box_sec) 
	-- return a div for a section of the box including child divs
	-- for each content field in that section
	-- local sec_div = mw.html.create('div'):cssText(data.styles.section[box_sec])	
	local sec_fields = {}
	for k,v in pairs(data.fields) do
		if data.fields[k].section == box_sec then
			local field = data.fields[k]
			field.title = mw.text.trim(frame:expandTemplate{title=args.translations, args={field.key}})
			field.values = {}
			if (args[k] and string.len(args[k]) > 0) then
				field.values[1] = args[k] --does not accept numbered args
			elseif field.isRequired == true then
				if field.vtype == "text" then
					field.values[1] = mw.text.trim(frame:expandTemplate{title=args.translations, args={field.default}})
				else
					field.values[1] = field.default
				end
			end
			local field_div = makeSectionFields(args, field)
			sec_fields[field.rank] = field_div
		end
	end
	local sec_div = makeSectionDiv(sec_fields, data.styles.section[box_sec]) 
	return sec_div
end		

function makeInfobox(frame, args, data, filled_role_data, open_roles)
	-- builds the infobox. Some content sections are required, others 
	-- are optional. Optional sections are defined in the stylesheet.
	local box = mw.html.create('div'):cssText(data.styles.box.outer)
	local inner_box = mw.html.create('div'):cssText(data.styles.box.inner)
	if data.sections.above == true then
		local sec_top = makeSection(frame, args, data, "above")
		box:node(sec_top)
	end	
	if data.sections.nav == true then
		local sec_nav = makeSection(frame, args, data, "nav")
		box:node(sec_nav)
	end		
	local sec_head = makeSection(frame, args, data, "head")
	inner_box:node(sec_head)
	local sec_main = makeSection(frame, args, data, "main")
	inner_box:node(sec_main)
	if data.sections.participants == true then
		local sec_participants = makeParticipantsSection(frame, args, data, filled_role_data, open_roles)
		inner_box:node(sec_participants)
	end
	if data.sections.cta == true then
		local sec_cta = makeSection(frame, args, data, "cta")
		inner_box:node(sec_cta)
		inner_box:tag('div'):cssText("clear:both"):done() --clears buttons in the cta sections
	end	
	inner_box:allDone()
	box:node(inner_box)
	if data.sections.below == true then
		local sec_bottom = makeSection(frame, args, data, "below")
		box:node(sec_bottom)
	end	
	box:allDone()
	return box			
end

function orderStringtoNumber(array, val, num)
    if num > table.getn(array) then
        array[#array+1] = val
    else
        table.insert(array, num, val)
    end  
    return array
end    
    
function isJoinable(args, data)
	if args.more_participants == "NO" then
		data.fields.join = nil
		data.fields.endorse.style = "display:inline; float:right;"
	end	
	return data
end	

function deepCopyTable(data)
	-- the deep copy is a workaround step to avoid the restrictions placed on 
	-- tables imported through loadData
	if type(data) ~= 'table' then return data end
	local res = {}
	for k,v in pairs(data) do
		if type(v) == 'table' then
		v = deepCopyTable(v)
		end
		res[k] = v
	end
	return res
end
	
function getPortalData(args)
	-- loads the relevant stylesheet, if a sub-template was called with a portal
	-- argument and a stylesheet exists with the same name. For example, calling 
	-- {{#invoke:Probox/Idealab|main|portal=Idealab}} would load the
	-- Module:Probox/Idealab stylesheet
	local data_readOnly = {}
	local data_writable = {}
	if (args.portal and mw.title.makeTitle( 'Module', 'Probox/' .. args.portal).exists) then
		data_readOnly = mw.loadData("Module:Probox/" .. args.portal)
	else
		data_readOnly = mw.loadData("Module:Probox/Defaults")
	end	
	-- data_writable = TableTools.shallowClone(data_readOnly)
	data_writable = deepCopyTable(data_readOnly)
	return data_writable
end

function string.starts(String,Start)
  return string.sub(String,1,string.len(Start))==Start
end

function stringToLowerCase(value)
	return mw.ustring.lower(value)
end	

function getRoleArgs(args, available_roles)
    -- returns:
    -- 1) a table of ordered values for valid role params, 
    -- even if numbered nonsequentially
    -- 2) a table of all roles with at least 1 empty param, 
    -- plus the default volunteer role
    local filled_role_data = {}
    local open_roles = {}
    if available_roles.default then -- some boxes have default role to join
    	open_roles[available_roles.default] = true
    end	
	for rd_key, rd_val in pairs(available_roles) do
		for a_key, a_val in pairs(args) do
    		if string.starts(a_key, rd_key) then
                if string.len(a_val) == 0 then
                    open_roles[rd_key] = true
                else   
                    if not filled_role_data[rd_key] then filled_role_data[rd_key] = {} end
                    local arg_num = tonumber(a_key:match('^' .. rd_key .. '([1-9]%d*)$'))
                    if arg_num then
                        filled_role_data[rd_key] = orderStringtoNumber(filled_role_data[rd_key], a_val, arg_num)
                    else
                        table.insert(filled_role_data[rd_key], 1, a_val)   
                    end
                end    
            end    
		end
	end
	return filled_role_data, open_roles
end	     

function p.main(frame)
	local args = getArgs(frame, {removeBlanks = false})
	local data = getPortalData(args)
 	data = isJoinable(args, data)
	if not (args.translations and mw.title.new("Template:" .. args.translations).exists) then
 		args.translations = "Probox/Content"
 	end
 	if data.sections.cta == true then
 		args.talk = tostring(mw.title.getCurrentTitle().talkPageTitle)  -- expensive
 	end	
 	if args.portal == "Idealab" then --special casing for IdeaLab only
 		args.toolkit = tostring(mw.title.getCurrentTitle().fullText) .. "/" .. "Toolkit"
 	end	
 	local filled_role_data, open_roles = getRoleArgs(args, data.roles)
	local box = makeInfobox(frame, args, data, filled_role_data, open_roles)
	local infobox = tostring(box)
	-- only add categories if the template is being transcluded into
	-- the Grants or User namespace, for now
	if (data.categories and (mw.title.getCurrentTitle().nsText == "Grants" or mw.title.getCurrentTitle().nsText == "User")) then
		page_categories = addCategories(data, args, open_roles)
		infobox = infobox .. page_categories
	end	
	return infobox
end

return p