Jump to content

Module:Events calendar

From Meta, a Wikimedia project coordination wiki
Module documentation

Usage

[edit]

{{#invoke:Events calendar|function_name}}


local p = {}
local lang_code

local function getDaysInMonth(month, year)
  local days_in_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }   
  local d = days_in_month[month]
   
  -- check for leap year
  if month == 2 then
    if math.mod(year,4) == 0 then
     if math.mod(year,100) == 0 then
      if math.mod(year,400) == 0 then                    
          d = 29
      end
     else
      d = 29
     end
    end
  end

  return d  
end

local function getFirstTimestamp(month, year, extended)
	first_timestamp = os.time({ year = year, month = month, day = 1, hour = 0, minut = 0 })
	if not extended then
		return first_timestamp
	end
	first_day = (os.date("*t", first_timestamp).wday - 2)%7
	return first_timestamp - (first_day * 86400)
end

local function getLastTimestamp(month, year, extended)
	last_timestamp = os.time({ year = year, month = month, day = getDaysInMonth(tonumber(month), tonumber(year)), hour = 23, minut = 59 })
	if not extended then
		return last_timestamp
	end
	last_day = (os.date("*t", last_timestamp).wday - 2)%7
	return last_timestamp + ((6-last_day) * 86400)
end

local function getFirstDayTimestamp(year, month, day)
	return os.time({ year = year, month = month, day = day, hour = 0 })
end

local function getLastDayTimestamp(year, month, day)
	return os.time({ year = year, month = month, day = day, hour = 23, min = 59, sec = 59 })
end

local function rruleToInterval(rrule)
	local days = 10000
	if rrule.freq == 'weekly' then
		days = 7
	end
	local interval = rrule.interval or 1
	return days * interval * 86400
end

local function currentDatesByRrule(dtstart, dtend, rrule, t1)
	local dtuntil = rrule['until'] or 2000000000 -- ~2033
	if dtuntil < t1 then
		return dtstart, dtend
	end
	
	local interval = rruleToInterval(rrule)
	local duration = dtend - dtstart
	while dtstart < t1 and dtstart + interval <= dtuntil do
		dtstart = dtstart + interval
	end
	dtend = dtstart + duration

	return dtstart, dtend
end

local function dateFilter(data, t1, t2)
	filtered_data = {}
	
	for _,value in pairs(data) do
		local dtstart = value.dtstart
		local dtend = value.dtend
		if value.rrule then
			dtstart, dtend = currentDatesByRrule(dtstart, dtend, value.rrule, t1)
		end
		if (dtstart >= t1 and dtstart <= t2)
			or (dtend >= t1 and dtend <= t2)
			or (dtstart < t1 and dtend > t2)
		then
			if dtstart < t1 then
				dtstart = t1
			end
			value.dtstart = dtstart
			value.dtend = dtend
			table.insert(filtered_data, value)
		end
	end
	
	return filtered_data
end

local function tagFilter(data, searched_tags)
	if #searched_tags == 0 then
		return data
	end
	
	filtered_data = {}
	
	for _,value in pairs(data) do
		if value.tags ~= nil then
			for _,tag in pairs(value.tags) do
				local break_out = false
				for _,searched_tag in pairs(searched_tags) do
					if tag == searched_tag then
						table.insert(filtered_data, value)
						break_out = true
						break
					end
				end
				if break_out then break end
			end
		end
	end
	
	return filtered_data
end

local function locationFilter(data, searched_locations)
	if #searched_locations == 0 then
		return data
	end
	
	filtered_data = {}
	
	for _,value in pairs(data) do
		for _,location in pairs(value.location) do
			local break_out = false
			for _,searched_location in pairs(searched_locations) do
				if location == searched_location then
					table.insert(filtered_data, value)
					break_out = true
					break
				end
			end
			if break_out then break end
		end
	end
	
	return filtered_data
end

local function geolocFilter(data)
	filtered_data = {}
	
	for _,value in pairs(data) do
		if value.geoloc.lat ~= nil and value.geoloc.lng ~= nil then
			table.insert(filtered_data, value)
		end
	end

	return filtered_data
end

local function formatEvents(data, short)
	local content = ""
	for _,event in pairs(data) do
		if content ~= "" then
			content = content .. "
"
end local title = event.title if event.link ~= nil and event.link ~= "" then if mw.ustring.find(event.link, 'https?://') == 1 then title = "[" .. event.link .. " " .. event.title .. "]" else title = "[[" .. event.link .. "|" .. event.title .. "]]" end end local langs = '' if event.langs ~= nil and #event.langs then for _, lang in ipairs( event.langs ) do langs = langs .. '' .. lang .. ' ' end end local date = os.date("*t", event.dtstart) local event_id = mw.text.split(event.id, '@', true)[1] -- Show event line with buttons to edit and download event. Both are hidden by default -- (display: none in [[Template:Events_calendar/styles.css]]) and are shown back -- when Events-calendar-navigation.js script is loaded in browser. content = content .. "'''" .. event.location[#(event.location)] .. " " .. string.format("%02d:%02d", date.hour, date.min) .. "''' " .. langs .. title .. " [[File:Blue pencil.svg|12px|link=|alt=Edit event]] [[File:Zorin Icon Themes – document-save-symbolic.svg|12px|link=|alt=Download event as Ical]]" if not short then content = content .. "
"
.. event.description
end end return content end local function arrayToText(array) if #array == 0 then return "" end local text = array[1] for i = 2, #array do text = array[i] .. "," .. text end return text end function p.ical(frame, year, month, locations, tags) local args = frame:getParent().args local today = os.date("*t") local timestamp = getFirstTimestamp(month, year, false) local last_timestamp = timestamp + 300000000 local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} }) data = dateFilter(data, timestamp, last_timestamp) data = tagFilter(data, tags) data = locationFilter(data, locations) local content = "BEGIN:VCALENDAR" .. "\r\nVERSION:2.0" .. "\r\nPRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN" .. "\r\nX-WR-CALNAME:" .. ( args.title == nil and "Wikimedia events" or args.title ) .. "\r\nREFRESH-INTERVAL;VALUE=DURATION:P1H" .. "\r\nX-PUBLISHED-TTL:PT1H" for _,value in pairs(data) do content = content .. "\r\nBEGIN:VEVENT" .. "\r\nCREATED:" .. os.date("%Y%m%dT%H%M%SZ", value.dtcreated) .. "\r\nLAST-MODIFIED:" .. os.date("%Y%m%dT%H%M%SZ", value.dtmodified) .. "\r\nDTSTAMP:" .. os.date("%Y%m%dT%H%M%SZ", value.dtmodified) .. "\r\nUID:" .. value.id .. "\r\nSUMMARY:" .. value.title if value.description ~= nil then content = content .. "\r\nDESCRIPTION:" .. value.description:gsub("\n", ", ") end content = content .. " " .. mw.text.encode(value.link:gsub(" ", "_")) if value.tags ~= nil then content = content .. "\r\nCATEGORIES:" .. arrayToText(value.tags) end if value.location ~= nil then content = content .. "\r\nLOCATION:" .. arrayToText(value.location) end if value.link ~= nil then if mw.ustring.find(value.link, 'https?://') == 1 then content = content .. "\r\nURL:" .. mw.text.encode(value.link:gsub(" ", "_")) else content = content .. "\r\nURL:https://meta.wikimedia.org/wiki/" .. mw.text.encode(value.link:gsub(" ", "_")) end end if value.rrule ~= nil then local rrule = {} for key, value in pairs( value.rrule ) do if key == 'until' then value = os.date( "%Y%m%dT%H%M%S", value ) end table.insert( rrule, string.upper( key ) .. '=' .. string.upper( value ) ) end content = content .. "\r\nRRULE:" .. table.concat( rrule, ';' ) end content = content .. "\r\nDTSTART:" .. os.date("%Y%m%dT%H%M%S", value.dtstart) .. "\r\nDTEND:" .. os.date("%Y%m%dT%H%M%S", value.dtend) .. "\r\nEND:VEVENT" end content = content .. "\r\nEND:VCALENDAR" return content end function p.grid(frame, year, month, locations, tags) local args = frame:getParent().args local today = os.date("*t") local timestamp = getFirstTimestamp(month, year, true) local last_timestamp = getLastTimestamp(month, year, true) local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} }) data = dateFilter(data, timestamp, last_timestamp) data = tagFilter(data, tags) data = locationFilter(data, locations) local grid_content = "" while timestamp < last_timestamp do local line_content = "" for i=1,7 do local current_date = os.date("*t", timestamp) local events = formatEvents( dateFilter(data, getFirstDayTimestamp(current_date.year, current_date.month, current_date.day), getLastDayTimestamp(current_date.year, current_date.month, current_date.day)), true ) local is_today = current_date.month == today.month and current_date.day == today.day and "yes" or "" local is_grey = current_date.month ~= tonumber(month) and "yes" or "" line_content = line_content .. frame:expandTemplate{ title = 'Events calendar/Table-cell grid', args = { day = current_date.day, event = events, grey = is_grey, today = is_today, lang = lang_code } } timestamp = timestamp + 86400 end grid_content = grid_content .. frame:expandTemplate{ title = 'Events calendar/Table-line grid', args = {cells = line_content, lang = lang_code} } end return frame:expandTemplate{ title = 'Events calendar/Table grid', args = {lines = grid_content, lang = lang_code} } end function p.list(frame, year, month, locations, tags) local args = frame:getParent().args local today = os.date("*t") local timestamp = getFirstTimestamp(month, year, false) local last_timestamp = getLastTimestamp(month, year, false) local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} }) data = dateFilter(data, timestamp, last_timestamp) data = tagFilter(data, tags) data = locationFilter(data, locations) local list_content = "" while timestamp < last_timestamp do local current_date = os.date("*t", timestamp) local events = dateFilter(data, getFirstDayTimestamp(current_date.year, current_date.month, current_date.day), getLastDayTimestamp(current_date.year, current_date.month, current_date.day)) if #events ~= 0 then local is_today = current_date.month == today.month and current_date.day == today.day and "yes" or "" list_content = list_content .. frame:expandTemplate{ title = 'Events calendar/Table-line list', args = { date = os.date("%Y-%m-%d", timestamp), lang = lang_code, events = formatEvents(events, false), today = is_today } } end timestamp = timestamp + 86400 end return frame:expandTemplate{ title = 'Events calendar/Table list', args = {lines = list_content, lang = lang_code} } end function p.map(frame, year, month, locations, tags) local args = frame:getParent().args local today = os.date("*t") local data = mw.text.jsonDecode(frame:expandTemplate{ title = ':Events calendar/events.json', args = {} }) data = dateFilter(data, getFirstTimestamp(month, year, false), getLastTimestamp(month, year, false)) data = tagFilter(data, tags) data = locationFilter(data, locations) data = geolocFilter(data) if #data == 0 then return frame:extensionTag("mapframe", "", {width = "980", height = "500", align = "center", frameless = "", zoom = 2, latitude = 32 , longitude = 5 }) end local geojson = { type = "FeatureCollection", features = {} } local first_timestamp_of_today = getFirstDayTimestamp(today.year, today.month, today.day) local last_timestamp_of_today = getLastDayTimestamp(today.year, today.month, today.day) local lang = mw.language.new( lang_code ) for _,value in pairs(data) do local title = value.title if value.link ~= nil and value.link ~= "" then if mw.ustring.find(value.link, 'https?://') == 1 then title = "[[" .. value.link .. "|" .. value.title .. "]]" else title = "[[" .. value.link .. "|" .. value.title .. "]]" end end local properties = { title = title, description = "" .. lang:formatDate( "l d F - H:i", os.date("%Y%m%d%H%M00", value.dtstart), false) .. "
"
.. value.description, } properties["marker-color"] = "#e23131" if value.dtend < first_timestamp_of_today then properties["marker-color"] = "#555555" elseif value.dtstart > last_timestamp_of_today then properties["marker-color"] = "#6d9ce8" end table.insert(geojson.features, { type = "Feature", properties = properties, geometry = { type = "Point", coordinates = { tonumber(value.geoloc.lng), tonumber(value.geoloc.lat), } } }) end return frame:extensionTag("mapframe", mw.text.jsonEncode(geojson), {width = "980", height = "500", align = "center", frameless = ""}) end function p.calendar(frame) local args = frame:getParent().args local display = args.display local year = args.year or os.date("%Y") local month = args.month or os.date("%m") local locations = (args.locations and args.locations ~= "") and mw.text.split( args.locations, ",", true ) or {} local tags = (args.tags and args.tags ~= "") and mw.text.split( args.tags, ",", true ) or {} lang_code = args.lang or frame:preprocess( '{{PAGELANGUAGE}}' ) local content = "" if display == "ical" then return p.ical(frame, year, month, locations, tags) elseif display == "list" then content = p.list(frame, year, month, locations, tags) elseif display == "map" then content = p.map(frame, year, month, locations, tags) else display = "grid" content = p.grid(frame, year, month, locations, tags) end return mw.text.tag{ name = "div", attrs = { ["class"] = "events-calendar", ["data-display"] = display, ["data-lang"] = lang_code, ["data-year"] = year, ["data-month"] = month, ["data-locations"] = #locations == 0 and "" or args.locations, ["data-tags"] = #tags == 0 and "" or args.tags, }, content = table.concat{ frame:extensionTag("templatestyles", "", {src = "Events calendar/styles.css"}), frame:expandTemplate{ title = "Events calendar/Header", args = {lang = lang_code, month = month, year = year} }, content, frame:expandTemplate{ title = "Events calendar/Button", args = {lang = lang_code} }, } } end return p