Module:Deployment schedule

From Wikitech
Module documentation

The deployment schedule module provides functions for formatting and organizing the deployment calendar.

formatTable

Produces the wrapper around each row in the table. You only need one of these per calendar. It accepts one option, hidedate which if set to true will display all dates as only the day of the week.

row

Workhorse of the calendar. Accepts options:

  • when Start date and time of the deployment. Expressed as some timestamp like 2014-05-12 8:00 PDT
  • length Length of deployment window in hours. Default is 2.
  • window Name of the window, usually the name of the component being modified (e.g. parsoid).
  • who Who is performing the deployment. Consider using the {{ircnick}} template.
  • what What is being deployed.

Example

{{#invoke:Deployment schedule|formatTable|hidedate=false|

{{#invoke:Deployment schedule|row
|when=2014-05-12 8:00 PDT
|length=1
|window=[[SWAT deploys|SWAT]] <br/><small>'''(Max 8 patches)'''</small>
|who=Nik (manybubbles), Brad (anomie)
|what=
''Tomasz (twkozlowski)''
* [config] Enable Extension:NewUserMessage on the Ukrainian Wikipedia {{gerrit|132747}}
''Timo (Krinkle)''
* [wmf3] VisualEditor i18n fix backport {{gerrit|132770}}; cite case sensitivity backport {{gerrit|132773}}
* [wmf4] VisualEditor i18n fix backport {{gerrit|132769}}; cite case sensitivity backport {{gerrit|132772}}
''Requesting developer'' (''irc handle'')
* ''Gerrit link to backport or config change''
}}

}}

Result

Time Component Deployer Changes

Monday, May 12

 UTC #
 PDT
SWAT
(Max 8 patches)
Nik (manybubbles), Brad (anomie) Tomasz (twkozlowski)
  • [config] Enable Extension:NewUserMessage on the Ukrainian Wikipedia 132747

Timo (Krinkle)

  • [wmf3] VisualEditor i18n fix backport 132770; cite case sensitivity backport 132773
  • [wmf4] VisualEditor i18n fix backport 132769; cite case sensitivity backport 132772

Requesting developer (irc handle)

  • Gerrit link to backport or config change

local p = {}

local timezones = {
	-- Add timezone offsets here!
	Z = 0,
	UTC = 0,
	PST = -8,
	PDT = -7,
	SF = 'America/Los_Angeles',
	DE = 'Europe/Berlin',
}
local timezones2 = {
	SF = 'America/Los_Angeles',
	DE = 'Europe/Berlin',
}

function getSanFranciscoTimezone( time )
	-- mw.getContentLanguage():formatDate() will handle input with tzdata
	-- timezones, outputting the corresponding time in UTC. So we pass it input
	-- that is manually offset for PST and PDT and see which one matches UTC.
	local utc = os.date( '!%FT%TZ', time )
	local pst = mw.getContentLanguage():formatDate( 'Y-m-d\\TH:i:s\\Z', os.date( '!%FT%T America/Los_Angeles', time + timezones.PST * 3600 ) )
	local pdt = mw.getContentLanguage():formatDate( 'Y-m-d\\TH:i:s\\Z', os.date( '!%FT%T America/Los_Angeles', time + timezones.PDT * 3600 ) )

	if utc == pst then
		return 'PST'
	elseif utc == pdt then
		return 'PDT'
	else
		mw.logObject( { UTC = utc, PST = pst, PDT = pdt }, 'Cannot determine SF time' )
		return 'PST'
	end
end

function trim( str )
	-- Trims a string because wikitech apparently doesn't have this library...
	str = string.gsub( str, "%s+$", "" )
	str = string.gsub( str, "^%s+", "" )
	return str
end


-- Re-usable version of stable attributes on the item element (style agnostic)
-- * 'id' for anchors
-- * 'utcstart' for start datetime in UTC (ISO format)
-- * 'utcend' for end datetime in UTC (ISO format)
function getStableItemAttribs(args)
	local start = args['when']
	local seconds = ( tonumber( args['length'] ) or 2 ) * 60 * 60
	if start == nil or start == '' then
		-- Fallback
		return { id = "", utcstart = "", utcend = "" }
	end

	year, month, day, hour, minute, tz_raw = string.match( start, '^(%d+)-(%d+)-(%d+) (%d+):(%d+) ([A-Z]+)$' )
	tz = timezones2[tz_raw] or tz_raw
	local formatted = string.format( '%d-%d-%dT%d:%d:00 %s',
		year or 1980,
		month or 0,
		day or 0,
		hour or 0,
		minute or 42,
		tz
	)
	utc = tonumber( mw.getContentLanguage():formatDate( 'U', formatted ) )

	local id = string.format( 'deploycal-item-%s', os.date( "!%Y%m%dT%H%M", utc ) )
	local utcstart = os.date( "!%Y-%m-%dT%H:%M+00:00", utc )
	local utcend = os.date( "!%Y-%m-%dT%H:%M+00:00", utc + seconds )

	return { id = id, utcstart = utcstart, utcend = utcend }
end

function p.item_stable_attribs( frame )
	local parentFrame = frame:getParent()
	local stable = getStableItemAttribs( parentFrame.args )
	return string.format(
		'id="%s" data-utcstart="%s" data-utcend="%s"',
		stable.id, stable.utcstart, stable.utcend
	)
end

function p.getJSON( frame )
	local parentFrame = frame:getParent()
	local tmp = {}
	for k, v in pairs(parentFrame.args) do
		tmp[k] = mw.text.nowiki( mw.ustring.gsub( v, '"', "'" ) )
	end
	local stable = getStableItemAttribs( parentFrame.args )
	tmp['id'] = stable.id
	return mw.text.jsonEncode( tmp )
end

-- Date fomatting logic for [[Template:Deployment_calendar_event_card]]
function p.when( frame )
	local start = frame.args['start']
	local seconds = ( tonumber( frame.args['hours'] ) or 2 ) * 60 * 60

	if start == nil or start == '' then
		return "No time given"
	end

	year, month, day, hour, minute, tz_raw = string.match( start, '^(%d+)-(%d+)-(%d+) (%d+):(%d+) ([A-Z]+)$' )
	tz = timezones2[tz_raw] or tz_raw
	local formatted = string.format( '%d-%d-%dT%d:%d:00 %s',
		year or 1980,
		month or 0,
		day or 0,
		hour or 0,
		minute or 42,
		tz
	)
	utc = tonumber( mw.getContentLanguage():formatDate( 'U', formatted ) )

	sftz = getSanFranciscoTimezone( utc )
	tzoffset = timezones[sftz]
	sflocal = utc + ( tzoffset * 60 * 60 )
		
	-- Determine if the SF day is different from the UTC day
	if os.date( "!%A", utc ) ~= os.date( "!%A", sflocal ) then
		sfdatestr = os.date( "!<small>(%a)</small> ", sflocal )
	else
		sfdatestr = ""
	end

	return string.format( '<span class="deploycal-time-utc">%s&ndash;%s&nbsp;UTC [[#deploycal-item-%s|#]]</span>' ..
		' <br/> ' ..
		'<span class="deploycal-time-sf">%s %s&ndash;%s&nbsp;%s</span>',

		'<time class="deploycal-item-utcstart deploycal-starttime" datetime="' .. os.date( "!%Y-%m-%dT%H:%M+00:00", utc ) .. '">' ..
			os.date( "!%H:%M", utc ) .. '</time>',

		'<time class="deploycal-item-utcend deploycal-endtime" datetime="' .. os.date( "!%Y-%m-%dT%H:%M+00:00", utc + seconds ) .. '">' ..
			os.date( "!%H:%M", utc + seconds ) .. "</time>",

		os.date( "!%Y%m%dT%H%M", utc ),

		sfdatestr,

		'<time class="deploycal-starttime"  datetime="' .. os.date( "!%Y-%m-%dT%H:%M+" .. tzoffset .. ":00", sflocal ) .. '">' ..
			os.date( "!%H:%M", sflocal ) .. "</time>",

		'<time class="deploycal-endtime" datetime="' .. os.date( "!%Y-%m-%dT%H:%M+" .. tzoffset .. ":00", sflocal + seconds ) .. '">' ..
			os.date( "!%H:%M", sflocal + seconds ) .. "</time>",
			
		sftz
	)
end


function p.header( frame )
	-- In my young and dumb days, this created a header for formatTable.
	-- We still need it because the archive page calls it (though it no longer needs to)
end

function p.footer( frame )
	-- In my young and dumb days, this created a footer for formatTable.
	-- We still need it because the archive page calls it (though it no longer needs to)
end


-- Code for the older table-based layout.
-- Kept to help render archived deployment calendars.
-- (TODO: Consider substituting them at some point if possible.)
function p.row( frame )
	-- Produces a raw row to be reconsumed by this script for sorting
	-- Expects:
	--- when, string, like YYYY-MM-DD HH:mm XXX where XXX is a valid timezone listed in timezones
	--- length, number, of hours the deploy will take
	--- window, string, what deployment window this is (e.g. SWAT, parsoid, MediaWiki)
	--- who, string, who is performing the deploy
	--- what, string, what will be deployed
	-- 
	-- Outputs: UTC Timestamp,Length,Who,What
	--- commas in strings are replaced with &#44;
	
	local when = frame.args['when']
	local length = ( tonumber( frame.args['length'] ) or 2 ) * 60 * 60
	local window = frame.args['window'] or "N/A"
	local who = frame.args['who'] or "Anonymous Cowards (must provide 'who=')"
	local what = frame.args['what'] or "Nothing? Then why is this an entry!? (must provide 'what=')"
	
	who = string.gsub( who, ",", "&#44;" )
	what = string.gsub( what, ",", "&#44;" )
	
	if when == nil then
		utc_time = 0
		what = "No time given (must provide 'when' and 'tz', e.g.: 'when=YYYY-MM-DD HH:mm XXX'"
	else
		year, month, day, hour, minute, tz = string.match( when, '^(%d+)-(%d+)-(%d+) (%d+):(%d+) ([A-Z]+)$' )
		tz = timezones[tz]
		
		if tz == nil then
			utc_time = 0
			what = "Unknown timezone in 'when' string! -- add it to the module :)"
		elseif type( tz ) == 'string' then
			local when2 = string.format( '%d-%d-%dT%d:%d:00 %s',
				year or 1980,
				month or 1,
				day or 1,
				hour or 1,
				minute or 1,
				tz
			)
			utc_time = mw.getContentLanguage():formatDate( 'U', when2 )
		else
			ts = os.time({
				year = year or 1980,
				month = month or 1,
				day = day or 1,
				hour = hour or 1,
				min = minute or 1
			})
			utc_time = ts - ( tz * 60 * 60 )
		end
	end
	
	return string.format( "%s,%s,%s,%s,%s", utc_time, length, window, who, what )
end
function p.formatTable( frame )
	-- Formats a bunch of rows from p.row()
	local rows = {}
	local row
	local count = 1
	local retval = {}
	local sflocal
	local sfdatestr
	local showdate = not ( frame.args['hidedate'] == 'true' )
	local tzoffset = 0
	local cutcday
	local sftz = "No data"
	
	-- Load and sort the arguments
	row = frame.args[count]
	while not ( row == nil ) do
		utc, length, window, who, what = string.match( row, '^(.*),(.*),(.*),(.*),(.*)$' )
		utc = tonumber( trim ( utc ) )
		length = tonumber( trim( length ) )
		who = trim( who )
		what = trim( what )
		
		table.insert( rows, { utc=utc, length=length, window=window, who=who, what=what } )
		count = count + 1
		row = frame.args[count]
	end
	table.sort( rows, function( a, b ) return a.utc < b.utc end )
	
	-- For each entry create the final row
	for count = 1, #rows do
		local datestr
		
		utc = rows[count].utc
		length = rows[count].length
		window = rows[count].window
		who = rows[count].who
		what = rows[count].what
		
		if ( cutcday ~= os.date( '%x', utc ) ) then
			-- Create a new date header in the table
			table.insert( retval, "|-\n| colspan='4' class='deploycal-dayrow' | \n==== " .. os.date( "!%A,&nbsp;%B&nbsp;%d", utc ) .. " ====" )
			cutcday = os.date( '%x', utc )
		end
		
		sftz = getSanFranciscoTimezone( utc )
		tzoffset = timezones[sftz]
		sflocal = utc + ( tzoffset * 60 * 60 )
		
		-- Determine if the SF day is different from the UTC day
		if os.date( "!%A", utc ) ~= os.date( "!%A", sflocal ) then
			sfdatestr = os.date( "!<small>(%a)</small> ", sflocal )
		else
			sfdatestr = ""
		end
		
		table.insert( retval, string.format( [=[ |- id="deploycal-item-%s" class="deploycal-item"
				| <span class="deploycal-time-utc">%s&ndash;%s&nbsp;UTC [[#deploycal-item-%s|#]]</span> <br/> <span class="deploycal-time-sf">%s %s&ndash;%s&nbsp;%s</span>
				| <span class="deploycal-window">%s</span>
				| %s
				| %s
			]=],
			os.date( "!%Y%m%dT%H%M", utc ),
			'<time class="deploycal-starttime" datetime="' .. os.date( "!%Y-%m-%dT%H:%M+00:00", utc ) .. '">' ..
				os.date( "!%H:%M", utc ) .. '</time>',
			'<time class="deploycal-endtime" datetime="' .. os.date( "!%Y-%m-%dT%H:%M+00:00", utc + length ) .. '">' ..
				os.date( "!%H:%M", utc + length ) .. "</time>",
			os.date( "!%Y%m%dT%H%M", utc ),
			sfdatestr,
			'<time class="deploycal-starttime"  datetime="' .. os.date( "!%Y-%m-%dT%H:%M+" .. tzoffset .. ":00", sflocal ) .. '">' ..
				os.date( "!%H:%M", sflocal ) .. "</time>",
			'<time class="deploycal-endtime" datetime="' .. os.date( "!%Y-%m-%dT%H:%M+" .. tzoffset .. ":00", sflocal + length ) .. '">' ..
				os.date( "!%H:%M", sflocal + length ) .. "</time>",
			sftz,
			window,
			who,
			what
		))
	end
	
	return string.format( [[
		{| class='wikitable deploycal'
			!Time
			!Component
			!Deployer
			!Changes
		]], sftz ) .. table.concat( retval, "\n" ) .. "\n|}"
end

return p