Module:Wikimédia France expenses

From Meta, a Wikimedia project coordination wiki
Module documentation
local p = {}


local STATES = { "request", "approval", "execution", "archiving", "verification" }
local NEXT_STATE = {
	request = "approval",
	approval = "execution",
	execution = "archiving",
	archiving = "verification",
	verification = nil,
	abort = nil
}
local ALLOWED_USERS = {
	request = nil,
	approval = {
		["Pyb"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["Pradigue"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["Rémy Gerbet WMFr"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["CindyDavidWMFr"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["0x010C"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["0x010D"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
	},
	execution = {
		["Pyb"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["Pradigue"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["Rémy Gerbet WMFr"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["CindyDavidWMFr"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["0x010C"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["0x010D"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
	},
	archiving = {
		["Jonathan Balima WMFr"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["0x010E"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
	},
	verification = {
		["Pradigue"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
		["0x010D"] = os.time{year=2099, month=1, day=1, hour=0, min=0, sec=0},
	}
}
ALLOWED_USERS["abort"] = ALLOWED_USERS["approval"]
local USERS_WITH_LIMITATIONS = {
	["Rémy Gerbet WMFr"] = 10000,
	["CindyDavidWMFr"] = 10000,
	["0x010C"] = 10000
}
local BOX_TEMPLATE = 'Wikimédia_France_expenses/box'
local STATE_TEMPLATE = 'Wikimédia France expenses/state'
local SIMPLE_TEMPLATE = 'Wikimédia France expenses/simple'
local SIMPLE_T_TEMPLATE = 'Wikimédia France expenses/simple-header'
local SIMPLE_B_TEMPLATE = 'Wikimédia France expenses/simple-footer'
local CREATE_TEMPLATE = 'Wikimédia France expenses/create'
local DATA_PAGE = ':Wikimédia_France/Finances/Dépenses/'
local USER_DATA_PAGE = 'WMFrDépenses.json'
local STATE_CATEGORY = {
	request = "Wikimédia France/Finances/Dépense/À approuver",
	approval = "Wikimédia France/Finances/Dépense/À exécuter",
	execution = "Wikimédia France/Finances/Dépense/À archiver",
	archiving = "Wikimédia France/Finances/Dépense/À vérifier",
	verification = "Wikimédia France/Finances/Dépense/Traitée",
	abort = "Wikimédia France/Finances/Dépense/Abandonnée"
}
local DATE_CATEGORY = "Wikimédia France/Finances/Dépense"
local ERROR_CATEGORY = "Wikimédia France/Finances/Dépense/Erreur de signature"
local DATE_REGEX = "(%d+)-(%d+)-(%d+)"


local lang = mw.language.new( 'fr' )
local signData = {}
local categories = {}


local function checkState( data, stateName )
	local user = data.state[ stateName ].u
	local date = data.state[ stateName ].t
	
	if ALLOWED_USERS[stateName] ~= nil then
		allowedDate = ALLOWED_USERS[stateName][user]
		if allowedDate == nil or date > allowedDate then
			return "L'utilisateur " .. user .. " n'est pas autorisé à signer cette étape."
		elseif (stateName == 'approval' or stateName == 'execution') and user == data.state['request'].u then
			return "Le demandeur n'a pas le droit de signer cette étape."
		elseif USERS_WITH_LIMITATIONS[user] ~= nil and data.amount > USERS_WITH_LIMITATIONS[user] then
			return "Montant trop élevé pour pouvoir être signé par " .. user .. "."
		end
	end
	
	return nil
end

local function getState(value)
	result = STATES[1]
	
	if value.state['abort'] ~= nil and value.state['abort'].u ~= nil then
		return 'abort'
	end
	
	for _,stateName in pairs(STATES) do
		if value.state[stateName] == nil or value.state[stateName].u == nil then
			break
		end
		if checkState( value, stateName ) ~= nil then
			break
		end
		
		result = stateName
	end
	
	return result
end


local function getInfoById(data, id)
	for _,value in pairs(data) do
	    if tonumber(value.id) == tonumber(id) then
	    	return value
    	end
	end
	
	return nil
end

local function convert_date(date)
	year, month, day =s:match(date)
	return os.time{year=year, month=month, day=day, hour=0, min=0, sec=0}
end

local function fromDateFilter(data, date)
	filtered_data = {}
	
	-- convert date to timestamp
	year, month, day = date:match(DATE_REGEX)
	date = os.time{year=year, month=month, day=day, hour=0, min=0, sec=0}
	
	for _,value in pairs(data) do
	    if value.state.request.t >= date then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function toDateFilter(data, date)
	filtered_data = {}
	
	-- convert date to timestamp
	year, month, day = date:match(DATE_REGEX)
	date = os.time{year=year, month=month, day=day, hour=23, min=59, sec=59}
	
	for _,value in pairs(data) do
	    if value.state.request.t <= date then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function nameFilter(data, name)
	filtered_data = {}
	name = mw.ustring.lower(mw.text.trim(name))
	
	for _,value in pairs(data) do
	    if mw.ustring.find( mw.ustring.lower(value.name), name ) ~= nil then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function reasonFilter(data, reason)
	filtered_data = {}
	reason = mw.ustring.lower(mw.text.trim(reason))
	
	for _,value in pairs(data) do
	    if mw.ustring.find( mw.ustring.lower(value.reason), reason ) ~= nil then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function minAmountFilter(data, amount)
	filtered_data = {}
	
	for _,value in pairs(data) do
	    if value.amount >= tonumber(amount) then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function maxAmountFilter(data, amount)
	filtered_data = {}
	
	for _,value in pairs(data) do
	    if value.amount <= tonumber(amount) then
	    	table.insert(filtered_data, value)
    	end
	end
	
	return filtered_data
end

local function projectFilter(data, projects)
	if #projects == 0 then
		return data
	end
	
	filtered_data = {}
	
	for _,value in pairs(data) do
		for _,project in pairs(projects) do
			project = mw.ustring.lower(mw.text.trim(project))
			if value.project ~= nil then
				if mw.ustring.find( mw.ustring.lower(value.project), project ) ~= nil then
					table.insert(filtered_data, value)
				end
			end
		end
	end
	
	return filtered_data
end

local function stateFilter(data, stateNames)
	if #stateNames == 0 then
		return data
	end

	filtered_data = {}
	
	for _,value in pairs(data) do
		currStateName = getState(value)
		for _, name in ipairs(stateNames) do
			if mw.ustring.lower(mw.text.trim(name)) == currStateName then
				table.insert(filtered_data, value)
				break
			end
		end
	end
	
	return filtered_data
end


local function getDigitalSignature(frame, user, id, stateName)
	local sign
	local title = mw.title.new( "User:" .. user .. '/' .. USER_DATA_PAGE )

	if title.exists then
		if signData[user] == nil then
			signData[user] = mw.text.jsonDecode(frame:expandTemplate{ title = "User:" .. user .. '/' .. USER_DATA_PAGE, args = {} })
		end
	
		if signData[user][id] ~= nil then
			return signData[user][id][stateName]
		elseif signData[user][tonumber(id)] ~= nil then
			return signData[user][tonumber(id)][stateName]
		end
	end
	
	table.insert( categories, ERROR_CATEGORY )
	return nil
end


local function _expense(frame, data, main_template, state_template)
	local states = ""
	local currentState = getState( data )
	local nextStateToSign = NEXT_STATE[ currentState ]
	
	if state_template ~= nil then
		for key,stateName in pairs(STATES) do
			local user = nil
			local date = nil
			local sign = nil
			local baduser = nil
			local isSelected = nil
			
			if stateName == 'approval' and data.state["abort"] ~= nil then
				stateName = 'abort'
			end
			if data.state[stateName] ~= nil then
				user = data.state[stateName].u
				date = data.state[stateName].t
			end
			
			if user ~= nil then
				-- get digital signature from user json subpage
				sign = getDigitalSignature(frame, user, data.id, stateName)
				
				-- check if the user is allowed to sign that state
				baduser = checkState( data, stateName )
				if baduser ~= nil then
					table.insert( categories, ERROR_CATEGORY )
				end
			end
			
			if stateName == nextStateToSign or ( stateName == 'abort' and baduser ~= nil ) then
				isSelected = "true"
			end
	
			states = states .. frame:expandTemplate{ title = state_template, args = {
				nb = key,
				name = stateName,
				user = user,
				date = lang:formatDate( "j F Y à H:i (e)", os.date("%Y%m%d%H%M00", date), false),
				timestamp = date,
				sign = sign,
				baduser = baduser,
				selected = isSelected,
			} }
			
			if stateName == 'abort' then
				break
			end
		end
	end
	
	table.insert( categories, STATE_CATEGORY[ currentState ] )
	table.insert( categories, DATE_CATEGORY )
	
	return frame:expandTemplate{ title = main_template, args = {
		id = data.id,
		date = os.date("%Y-%m-%d", data.state.request.t),
		applicant = data.state.request.u,
		name = data.name,
		project = data.project,
		reason = data.reason,
		amount = data.amount,
		comment = data.comment,
		state = currentState,
		states = states
	} }
end


local function dateToPageName(timestamp)
	local year = tonumber( os.date('%Y', timestamp) )
	local month = tonumber( os.date('%m', timestamp) )
	
	if month < 7 then
		year = year - 1
	end
	
	return DATA_PAGE .. tostring( year ) .. '-' .. tostring( year + 1 ) .. '.json'
end


function p.main(frame)
	local args = frame:getParent().args
	
	local format = args.format
	local search = args.search
	local create = args.create
	local id = args.id
	local exercice = tonumber(args.exercice)
	local name = args.name
	local reason = args.reason
	local project = (args.project == nil or args.project == "") and {} or mw.text.split(args.project, ",", true )
	local dateFrom = args.from
	local dateTo = args.to
	local amountMin = args.min
	local amountMax = args.max
	local state = (args.state == nil or args.state == "") and {} or mw.text.split( args.state, ",", true )
	
	local searchOptions = ''
	local text = ''
	
	if format ~= nil then
		searchOptions = 'data-format="' .. format .. '"'
	end
	
	if id ~= nil then
		searchOptions = searchOptions .. ' data-id="' .. id .. '"'
		local data = mw.text.jsonDecode(frame:expandTemplate{ title = dateToPageName(id), args = {} })
		data = getInfoById( data, id )
		
		if data ~= nil then
			text = _expense(frame, data, BOX_TEMPLATE, STATE_TEMPLATE)
			
			local categoriesString = ""
			for _,cat in pairs(categories) do
				categoriesString = categoriesString .. "\n[[Category:" .. cat .. "/" .. os.date("%Y", data.state.request.t) .. "]]"
			end
			
			return text .. "\n" .. categoriesString
		end
	elseif exercice ~= nil then
		searchOptions = searchOptions .. ' data-exercice="' .. exercice .. '"'
		local data = mw.text.jsonDecode(frame:expandTemplate{ title = DATA_PAGE .. tostring( exercice ) .. '-' .. tostring( exercice + 1 ) .. '.json', args = {} })
		
		if #project > 0 then
			searchOptions = searchOptions .. ' data-project="' .. args.project .. '"'
			data = projectFilter(data, project)
		end
		if dateFrom ~= nil then
			searchOptions = searchOptions .. ' data-from="' .. dateFrom .. '"'
			data = fromDateFilter(data, dateFrom)
		end
		if dateTo ~= nil then
			searchOptions = searchOptions .. ' data-to="' .. dateTo .. '"'
			data = toDateFilter(data, dateTo)
		end
		if name ~= nil then
			searchOptions = searchOptions .. ' name="' .. name .. '"'
			data = nameFilter(data, name)
		end
		if reason ~= nil then
			searchOptions = searchOptions .. ' reason="' .. reason .. '"'
			data = reasonFilter(data, reason)
		end
		if amountMin ~=nil then
			searchOptions = searchOptions .. ' data-min="' .. amountMin .. '"'
			data = minAmountFilter(data, amountMin)
		end
		if amountMax ~=nil then
			searchOptions = searchOptions .. ' data-max="' .. amountMax .. '"'
			data = maxAmountFilter(data, amountMax)
		end
		if #state > 0 then
			searchOptions = searchOptions .. ' data-state="' .. args.state .. '"'
			data = stateFilter(data, state)
		end
		
		if format == 'simple' then
			text = text .. frame:expandTemplate{ title = SIMPLE_T_TEMPLATE, args = {} } .. '\n'
			if #data > 0 then
				for _,value in pairs(data) do
					text = text .. _expense(frame, value, SIMPLE_TEMPLATE, nil) .. '\n'
				end
			else
				text = text .. '|-\n| colspan="8" | Aucune dépense correspondant aux critères.\n'
			end
			text = text .. frame:expandTemplate{ title = SIMPLE_B_TEMPLATE, args = {} }
		else
			for _,value in pairs(data) do
				text = text .. _expense(frame, value, BOX_TEMPLATE, STATE_TEMPLATE) .. "<br>"
			end
		end
	end
	
	if search ~= nil then
		text = '<div class="eb-search"><div class="eb-search-inputs" ' .. searchOptions .. '></div><div class="eb-search-results">\n' .. text .. '\n</div></div>'
	end
	
	if create ~= nil then
		text = text .. '\n' .. frame:expandTemplate{ title = CREATE_TEMPLATE, args = {} }
	end
	
	return text
end


return p