Loading
  • 21 Aug, 2019

  • By, Wikipedia

Module:Signpost

This module makes indexes of articles for the Signpost. It has two main functions:


Article data

The article data is stored in index modules organised by year, like Module:Signpost/index/2005, Module:Signpost/index/2006, etc. You can see the full list of index modules here. The data for all Signpost articles for a given year goes in that year's index module.

It is possible to edit the index modules by hand, but the recommended way is to use the SignpostTagger gadget. With SignpostTagger you can edit article data directly from Signpost article pages, and using it eliminates the possibility of making formatting errors or putting the data in the wrong place.

Data points

The index modules each contain a list of articles for that year. Each article has the following data points:

  • date: The date the article was published, in YYYY-MM-DD format
  • subpage: The subpage of the article. For example, for the special report "Adminship from the German perspective" located at Wikipedia:Wikipedia Signpost/2012-10-22/Special report, the subpage is "Special report".
  • title: The article's title.
  • authors: Array listing authors of the article, as read by Wegweiser and SignpostTagger (based on displayed text from the author field, not links).
  • tags: A list of tags for the article. For example, the special report mentioned above has the tags "germanwikipedia", "reformingrfa", "requestsforadminship", and "specialreport" (optional).
  • views: Set of key-value pairs for views over seven intervals: d007 (7 days), d015 (15 days), d030, d060, d090, d120, d180.
  • subhead: Article subheading (not normally displayed in the article, but can be parsed out of the "RSS description" template).
  • piccy: Set of key-value pairs for associating an image with an article (optional).
filename: Image credit to display as overlay (i.e. author of image).
license: Short string for image license in overlay (i.e. "CC 4.0 BY-SA").
scaling: Scaling, width-based: default is 300, which is the width of the snippet template.
xoffset: X-offset (i.e. how many pixels to crop from the left if scaling gives an image wider than 300px).
yoffset: Y-offset (i.e. how many pixels to crop from the top if scaling gives an image taller than 300px).

Tags and aliases

Tags are used to classify articles. For example, you can find all of the humourous articles by using the article list maker template to list all articles with the tag "humour". It is also possible to specify aliases for tags. For example, "humor" is an alias for the "humour" tag, so when you are tagging an article with SignpostTagger you can use either one of them. Tag aliases are stored at Module:Signpost/aliases.

Subpages

All subpages of this module

Test cases

Wikipedia:Wikipedia Signpost/Templates/Article list maker, sort by tag (arbitrationreport), limit 5, include subhead

2024-01-10, News and notes: In other news ... see ya in court!, by Andreas Kolbe
Let the games begin! The 2024 WikiCup is off to a strong start. With copyright enforcement, AI training and freedom of expression, it's another typical week in the wiki-sphere!
2023-12-04, News and notes: Beeblebrox ejected from Arbitration Committee following posts on Wikipediocracy, by Sdkb
Just as his term was ending!
2023-11-20, News and notes: Update on Wikimedia's financial health, by Andreas Kolbe
Plus: Sockpuppet investigators asking for help.
2023-11-06, News and notes: Board candidacy process posted, editors protest WMF privacy measure, sweet meetups, by Andreas Kolbe
And three new admins!
2023-10-23, News and notes: Where have all the administrators gone?, by Andreas Kolbe
Long time passing

local INDEX_MODULE = 'Module:Signpost/index'
local lang = mw.language.getContentLanguage()
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg

--------------------------------------------------------------------------------
-- Article class
--------------------------------------------------------------------------------

local Article = {}
Article.__index = Article

Article.viewSpans = {7, 15, 30, 60, 90, 120, 180}

Article.rowMethods = {
	page     = 'getPage',
	fullpage = 'getFullPage',
	date     = 'getDate',
	title    = 'getTitle',
	subpage  = 'getSubpage',
	author   = 'getAuthor',
	subhead  = 'getSubhead',
}

-- Register viewsNN row methods
for _, span in ipairs(Article.viewSpans) do
	Article.rowMethods["views" .. span] = "getViews" .. span
end

function Article.new(data)
	local self = setmetatable({}, Article)
	self.data = data
	self.matchedTags = {}
	return self
end

function Article:getSortKey()
	return self.data.sortKey
end

function Article:getPage()
	return self.data.page
end

function Article:getDate()
	return self.data.date
end

function Article:getTitle()
	return self.data.title
end

function Article:getSubpage()
	return self.data.subpage
end

function Article:getAuthor()
	return self.data.authors[1]
end

function Article:getAuthors()
	return self.data.authors
end

-- Add getViewsNN methods
for _, span in ipairs(Article.viewSpans) do
	Article["getViews" .. span] = function(self)
		if self.data.views then
			return self.data.views[string.format("d%03d", span)]
		else
			return nil
		end
	end
end

function Article:getSubhead()
	return self.data.subhead
end

function Article:getFragment()
	local fragment = self:getMatchedTags()[1]
	if fragment then
		return mw.uri.anchorEncode(fragment)
	end
end

function Article:getFullPage()
	local page = self:getPage()
	local fragment = self:getFragment()
	if fragment then
		return page .. '#' .. fragment
	else
		return page
	end
end

function Article:addMatchedTag(tag)
	table.insert(self.matchedTags, tag)
end

function Article:getMatchedTags()
	table.sort(self.matchedTags)
	return self.matchedTags
end

function Article:hasAllTags(t)
	local tags = self.data.tags
	for i, testTag in ipairs(t) do
		local hasTag = false
		for j, tag in ipairs(tags) do
			if tag == testTag then
				hasTag = true
			end
		end
		if not hasTag then
			return false
		end
	end
	return true
end

function Article:makeRowArgs()
	local methods = self.rowMethods
	local args = setmetatable({}, {
		__index = function (t, key)
			local method = methods[key]
			if method then
				return self[method](self)
			else
				error(string.format(
					"'%s' is not a valid parameter name",
					key
				), 2)
			end
		end
	})
	return args
end

function Article:renderTemplate(template, frame)
	frame = frame or mw.getCurrentFrame()
	local args = {}
	for key, method in pairs(self.rowMethods) do
		args[key] = self[method](self)
	end
	return frame:expandTemplate{
		title = template,
		args = args
	}
end

function Article:renderFormat(format)
	local args = self:makeRowArgs(articleObj)
	local ret = format:gsub('(%${([%a%d]+)})', function (match, key)
		return args[key] or match
	end)
	return ret
end

--------------------------------------------------------------------------------
-- List class
--------------------------------------------------------------------------------

local List = {}
List.__index = List

function List.new(options)
	checkType('List.new', 1, options, 'table')
	checkTypeForNamedArg('List.new', 'args', options.args, 'table', true)
	local self = setmetatable({}, List)
	self.index = options.index or mw.loadData(INDEX_MODULE)
	self.frame = options.frame or mw.getCurrentFrame()
	local args = options.args or {}

	-- Set output formats
	if not options.suppressFormatErrors
		and args.rowtemplate
		and args.rowformat
	then
		error("you cannot use both the 'rowtemplate' and the 'rowformat' arguments", 2)
	elseif not options.suppressFormatErrors
		and not args.rowtemplate
		and not args.rowformat
	then
		error("you must use either the 'rowtemplate' or the 'rowformat' argument", 2)
	else
		self.rowtemplate = args.rowtemplate
		self.rowformat = args.rowformat
	end
	if args.rowseparator == 'newline' then
		self.rowseparator = '\n'
	else
		self.rowseparator = args.rowseparator
	end
	self.noarticles = args.noarticles
	
	-- Get article objects, filtered by page, date and tag, and sort them.
	if args.page then
		self.articles = { self:getPageArticle(args.page) }
	elseif args.date then
		self.articles = self:getDateArticles(args.date)
	else
		self.articles = self:getTagArticles(args.tags, args.tagmatch)
		if not self.articles then
			self.articles = self:getAllArticles()
		end
		self:filterArticlesByDate(args.startdate, args.enddate)
		self:filterArticlesByAuthor(args.author)
	end
	self:sortArticles(args.sortdir, args.sortfield)
	if (args.limit and tonumber(args.limit)) or (args.start and tonumber(args.start)) then
		self:limitArticleCount(tonumber(args.start), tonumber(args.limit))
	end
	return self
end

-- Static methods

function List.normalizeDate(date)
	if not date then
		return nil
	end
	return lang:formatDate('Y-m-d', date)
end

-- Normal methods

function List:parseTagString(s)
	local ret = {}

	-- Remove whitespace and punctuation
	for i, tag in ipairs(mw.text.split(s, ',')) do
		tag = mw.ustring.gsub(tag, '[%s%p]', '')
		if tag ~= '' then
			tag = mw.ustring.lower(tag)
			table.insert(ret, tag)
		end
	end

	-- Resolve aliases
	for i, tag in ipairs(ret) do
		ret[i] = self.index.aliases[tag] or tag
	end
	
	-- Remove duplicates
	local function removeDuplicates(t)
		local vals, ret = {}, {}
		for i, val in ipairs(t) do
			vals[val] = true
		end
		for val in pairs(vals) do
			table.insert(ret, val)
		end
		table.sort(ret)
		return ret
	end
	ret = removeDuplicates(ret)

	return ret
end

function List:getPageArticle(page)
	local data = self.index.pages[page]
	if data then
		return Article.new(data)
	end
end

function List:getDateArticles(date)
	date = self.normalizeDate(date)
	local dates = self.index.dates[date]
	local ret = {}
	if dates then
		for i, data in ipairs(dates) do
			ret[i] = Article.new(data)
		end
	end
	return ret
end

function List:getTagArticles(s, tagMatch)
	if not s then
		return nil
	end
	local tagIndex = self.index.tags
	local ret, pages = {}, {}
	local tags = self:parseTagString(s)
	for i, tag in ipairs(tags) do
		local dataArray = tagIndex[tag]
		if dataArray then
			for i, data in ipairs(dataArray) do
				local obj = Article.new(data)
				-- Make sure we only have one object per page.
				if pages[obj:getPage()] then
					obj = pages[obj:getPage()]
				else
					pages[obj:getPage()] = obj
				end
				-- Record which tag we matched.
				obj:addMatchedTag(tag)
			end
		end
	end
	for page, obj in pairs(pages) do
		if not tagMatch
			or tagMatch == 'any'
			or tagMatch == 'all' and obj:hasAllTags(tags)
		then
			table.insert(ret, obj)
		end
	end
	return ret
end

function List:getAllArticles()
	local ret = {}
	for i, data in ipairs(self.index.list) do
		ret[i] = Article.new(data)
	end
	return ret
end

function List:getArticleCount()
	return #self.articles
end

function List:filterArticlesByDate(startDate, endDate)
	startDate = self.normalizeDate(startDate) or '2005-01-01'
	endDate = self.normalizeDate(endDate) or lang:formatDate('Y-m-d')
	local ret = {}
	for i, article in ipairs(self.articles) do
		local date = article:getDate()
		if startDate <= date and date <= endDate then
			table.insert(ret, article)
		end
	end
	self.articles = ret
end

function List:filterArticlesByAuthor(targetAuthor)
	if not targetAuthor then
		return
	end
	local ret = {}
	for i, article in ipairs(self.articles) do
		for j, author in ipairs(article:getAuthors()) do
			if author == targetAuthor then
				table.insert(ret, article)
			end
		end
	end
	self.articles = ret
end

function List:sortArticles(direction, field)
	local accessor
	if not field or field == 'date' then
		accessor = function (article) return article:getSortKey() end
	elseif field == 'page' then
		accessor = function (article) return article:getPage() end
	elseif field == 'title' then
		accessor = function (article) return article:getTitle() end
	else
		error(string.format("'%s' is not a valid sort field", field), 2)
	end
	local sortFunc
	if not direction or direction == 'ascending' then
		sortFunc = function (a, b)
			return accessor(a) < accessor(b)
		end
	elseif direction == 'descending' then
		sortFunc = function (a, b)
			return accessor(a) > accessor(b)
		end
	else
		error(string.format("'%s' is not a valid sort direction", direction), 2)
	end
	table.sort(self.articles, sortFunc)
end

function List:limitArticleCount(start, limit) 
	local ret = {}
	for i, article in ipairs(self.articles) do 
		if limit and #ret >= limit then
			break
		end
		if not start or i > start then
			table.insert(ret, article)
		end
	end
	self.articles = ret
end

function List:renderRow(articleObj)
	if self.rowtemplate then
		return articleObj:renderTemplate(self.rowtemplate, self.frame)
	elseif self.rowformat then
		return articleObj:renderFormat(self.rowformat)
	else
		error('neither rowtemplate nor rowformat were specified')
	end
end

function List:__tostring()
	local ret = {}
	for i, obj in ipairs(self.articles) do
		table.insert(ret, self:renderRow(obj))
	end
	if #ret < 1 then
		return self.noarticles
			or '<span style="font-color: red;">' ..
			'No articles found for the arguments specified</span>'
	else
		return table.concat(ret, self.rowseparator)
	end
end

--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------

local p = {}

local function makeInvokeFunc(func)
	return function (frame, index)
		local args = require('Module:Arguments').getArgs(frame, {
			parentOnly = true
		})
		return func(args, index)
	end
end

function p._exportClasses()
	return {
		Article = Article,
		List = List
	}
end

function p._count(args, index)
	local list = List.new{
		args = args,
		index = index,
		suppressFormatErrors = true
	}
	return list:getArticleCount()
end

p.count = makeInvokeFunc(p._count)

function p._main(args, index)
	return tostring(List.new{args = args, index = index})
end

p.main = makeInvokeFunc(p._main)

return p