Module:SSC base

From Semantic MediaWiki - Sandbox

This is a base class to be inherited by special entity classes. It provides basic functionality like argument plausibility tests, semantic storage, display of an infobox, etc.

Access to the semantíc data (reading and writing) is provided by Extension:Semantic Scribunto.

Usage

To build an entity class, create an appropriate config module. Use Module:SSC base/config as a template. Then put the following code on a module page to build the entity class:

local config = mw.loadData( 'Module:<entity module>/config' )
local baseClass = require( 'Module:SSC base' )

local class = baseClass:new( config )

return class

Suggested article structure for new sub classes

Module:Entity
Holds all the functions, that can be invoked on a template or a normal page
Module:Entity/class
Here you inherit from Module:SSC base and extend your entity class
Module:Entity/config
Holds the configuration for your entity. A template can be found on Module:SSC base/config.

For templates (as in boilerplates), please see below.

"Abstract" methods

There are several methods in this class that should/can be implemented by your entity class.

setDefaults( data )

Can be used to insert some defaults into the datastream after arguments from the template call are processed but before they are stored semantically. Here is the husk:

function class:setDefaults( data )
	local data = data
	-- for example
	data.title = data.title or mw.title.getCurrentTitle().text
	return data
end

data is a table that holds your arguments after preprocessing. Insert your defaults but don't forget to return the data table at the end.

alterDataAfterStorage( data )

Manipulates the data table after semantic data is stored but before the infobox is displayed.

function class:alterDataAfterStorage( data )
	local data = data
	-- for example
	data.header1 = 'Base data'
	return data
end

Note: a field in your data table is only added to your infobox, if the class' configuration says so.

Public methods

renderPage()

This is usually called in a function, which is invoked on the entity's module page. It provides this functions:

  1. filter known and existant argument data from template arguments
  2. checks for existance of mandatory arguments
  3. converts all list fields into a table
  4. saves the semantic data
  5. builds a nice infobox (utilizing Module:Infobox)
  6. places the page in the appropriate category
local entity = require( 'Module:<entity module>/class' ):new()
return entity:renderPage()

retrieveClassData( filter )

This can be used to retrieve all data stored for entities of that class.

If you want a subset, use string filter like the selector in an ask query (e.g. '[[property::value]] [[property2::value2]]').

local entityClass = require( 'Module:<entity module>/class' )
local classData = entityClass:retrieveClassData()
-- now classData is a table of up to 500 rows, each containing data about an entity
-- each row is a table, indexed by fieldnames as defined in the parameters table in entity class' config

Module boilerplates

You need a base module. This bridges your template and your class module and "exposes" some functions.

Boilerplate for your Module:Entity
local p = {}

function p.main( frame )
	local entity = require( 'Module:Entity/class' ):new()
	return entity:renderPage()
end

return p

Then there is the class, which inherits its main functionality from Module:SSC base. Remember, that you should fill the two "abstract" methods setDefaults() and alterDataAfterStorage.

Boilerplate for your Module:Entity/class
local config = mw.loadData( 'Module:Entity/config' )
local class = require( 'Module:SSC base' )

local entity = class:new( config )

function entity:alterDataAfterStorage( data )
	local data = data

	if not data.nonEmtyField then
		data.profession = 'unknown'
	end
	
	if data['date of birth'] and mw.ustring.find( data['date of birth'], '/', 1, true ) then
		local date = mw.text.split( data['date of birth'], '/', true )
		data['date of birth'] = date[2] .. '.' .. date[1] .. '.' .. date[3]
	end
	
	return data
end

function entity:setDefaults( data )
	local data = data
	data.title = data.title or mw.title.getCurrentTitle().text
	return data
end

function entity:addSomeData()
	local cbData, query = comicBooks:retrieveClassData( '[[Has comic book writer::' .. mw.title.getCurrentTitle().text .. ']]' )
	cbData = cbData or {}
	return '\n=== Works ===\n' .. self:extractWorks( cbData ) .. '\n'
		.. '=== Collaborations ===\n' .. self:extractCollaborations( cbData ) .. '\n'
end

function entity:renderPage()
	return class.renderPage( self )
		.. self:addSomeData()
end

return person

Finally, each class needs a config file. Here is the structure to fill:

Boilerplate for your Module:Entity/config
return {
	-- **********************
	-- * mandatory settings *
	-- **********************
	
	-- this is your type of entity
	entityType = '',
	
	-- this is the category, the entities of the class will be put in
	category = '',
	
	-- lists all available pairs "template parameter" -> "semantic property"
	-- if you want to have a field in the template without storing it semantically, set it to true
	parameters = {
		-- example entries
		firstname			= 'has firstname',
		lastname			= 'has lastname',
		nostore				= true,
	},

	-- tell the class, which fields are mandatory
	-- for every argument in this list not present on your page's template call,
	-- an error will be displayed
	mandatory = { 'firstname', 'firstname', ... },
	
	-- these fields possibly contain more than one value
	listFields = { '' },
	
	-- separator used in list fields
	delimiter = ',',

	-- *********************
	-- * optional settings *
	-- *********************
	-- the headline of the entity's page
	headline = 'This is a page about an entity',

	-- INFOBOX CONFIG HERE
	-- here you can disable your infobox
	omitInfoBox = false,

	-- this is the name of the field, used as title in the infobox
	titleField = 'name',
	
	-- this defines, which fields are put into your infobox and in which order
	infoboxConfig = {
		-- for every row in your infobox, add a table here, containing at least the entry "field" which
		-- refers to the data field to display. if you omit the entry "label", the field will be displayed
		-- over both columns
		{ field = 'firstname', label = 'Firstname' },
		{ field = 'lastname', label = 'Lastname' },
		{ field = 'nostore', label = 'No Store' },
		-- why is this not defined as an array: lua does not maintain the order of items in an array but accesses them randomly
	},
	
	-- configure here, which fields should be linked (or form-linked)
	linkFields = {
		-- if a field is set to true, it will be linked
		-- if it is set to a string, #formlink will be used
		firstname	= true,
		formEntry	= 'FormForEntrys'
	},
}

Example

See Module:Person/class, method person:getPersonVitae() for an example.


-- non class functions
local formatError = function( text )
	return '<div style="color:red; font-weight:700">' .. text .. '</div>\n'
end

local in_array = function( needle, haystack )
	local invertedHaystack = {}
	for _, v in pairs( haystack ) do
		invertedHaystack[v] = true
	end
	return invertedHaystack[needle]
end

-- start the class
local class = {}

function class:new( config )
	local config = config
	
	local o = {}
	setmetatable( o, self )
	self.__index = self
	
	-- store class configuration
	if config then
		if self.config and self.config.entityType then
			error ( 'config already set (by ' .. self.config.entityType .. ') but new is provided (by ' .. config.entityType .. ')' )
		end
		o.config = config
	end
	
	return o
end

function class:renderPage()

	local data
	local output = ''	
	local errorText = ''

	if self.config.headline and #self.config.headline > 0 then
		output = output .. '== ' .. self.config.headline .. ' ==\n'
	end
	
	data = self:getObjectDataFromArguments( self.config.parameters )
	
	data, errorText = self:prepareData( data )
	output = output .. errorText

	self:storeSemanticData( data, self.config.parameters )
	
	data = self:alterDataAfterStorage( data )
	
	if not self.config.omitInfoBox then
		output = self:addInfobox( output, data )
	end
	
	return self:addCategory( output )
end

function class:addCategory( output )
	local text = text
	if not in_array( mw.title.getCurrentTitle().namespace, { 10, 11, 828, 829 } ) and self.config.category then
		output = output .. '[[Category:' .. self.config.category .. ']]'
	end
	return output
end

function class:addInfobox( output, data )
	local data = data or {}
	local output = output
	
	if type( data ) == 'table' and self.config.infoboxConfig  and type( self.config.infoboxConfig  ) == 'table' then

		-- this array holds all attributes for the infobox
		-- see Module:Infobox for documentation
		local ibArgs = {
			aboveclass = 'objtitle titletext',
			headerclass = 'headertext',
			labelstyle = 'width: 30%;',
			datastyle = 'width: 70%;',
		}
		if self.config.entityType then
			ibArgs.bodyclass = 'infobox_' .. mw.ustring.gsub( mw.ustring.lower( self.config.entityType ), ' ', '_' )
			ibArgs.subheader = self.config.entityType
		end
		
		if self.config.titleField and data[self.config.titleField] then
			ibArgs.title = data[self.config.titleField]
		else
			ibArgs.title = data.title or data.name
		end

		local counter = 1 -- needed to set the correct arguments for the ibArgs table
		for _, row in pairs ( self.config.infoboxConfig ) do
			if row.field and data[row.field] then
				if row.label then
					ibArgs['label' .. counter] = row.label
				end
				ibArgs['data' .. counter] = self:getPrintout( row.field, data[row.field] )
				counter = counter + 1
			end
		end
	
		output = require( 'Module:Infobox' ).infobox( ibArgs ) .. '\n' .. output
	else
		error( 'An infobox was requested, but no class configuration for an infobox is present!' )
	end
	
	return output
end

function class:buildArrayIndexedByField(data, field)
	local result = {}
	local data = data
	for _, v in pairs(data) do
		if v[field] and not result[v[field]] then
			result[v[field]] = v
		end
	end
	return result
end

function class:checkForMandatoryFields( data, mandatoryFields )
	local data = data
	local mandatory = mandatoryFields or {}
	local errorText = ''
	if type( mandatory ) == 'table' then
		for _, m in pairs( mandatory ) do
			if not data[m] then
				errorText = errorText .. formatError( 'Mandatory argument "' .. m .. '" is missing!' )
			end
		end
	end
	return data, errorText
end

function class:className()
	return self.config.entityType or 'UNKNOWN'
end

function class:convertListFields( data, listFields )
	local data = data
	local listFields = listFields or {}
	local delimiter = self.config.delimiter or ','
	
	if type( listFields ) == 'table' then
		for _, lf in pairs( listFields ) do
			if data[lf] then
				data[lf] = mw.text.split( data[lf], delimiter, true )
				-- clear empty fields
				for k, v in pairs( data[lf] ) do
					v = mw.text.trim(v)
					if #v == 0 then
						data[lf][k] = nil
					else
						data[lf][k] = v
					end
				end
			end
			if data[lf] and #data[lf] > 0 then
				if #data[lf] == 1 then
					data[lf] = data[lf][1]
				end
			else
				data[lf] = nil
			end
		end
	end
	return data
end

function class:formatToolTip( data )
	local data = data or {}
	local output = ''
	
	if type( data ) == 'table' and self.config.infoboxConfig  and type( self.config.infoboxConfig  ) == 'table' then

		if self.config.titleField and data[self.config.titleField] then
			output = data[self.config.titleField]
		else
			output = data.title or data.name
		end
		if #output > 0 then
			output = '<b>' .. output .. '</b>'
		end
		
		if self.config.entityType then
			output = output .. ' (' .. self.config.entityType .. ')'
		end
		output = output .. '<br>'

		for _, row in pairs ( self.config.infoboxConfig ) do
			if row.field and data[row.field] then
				if row.label then
					output = output .. row.label .. ': '
				end
				output = output .. self:getPrintout( row.field, data[row.field] ) .. '<br>'
			end
		end
		return output
	else
		return 'A tooltip was requested, but no class configuration for an infobox is present!'
	end
end


function class:getObjectDataFromArguments( parametersLookup )
	
	local args = require( 'Module:Arguments' ).getArgs( mw.getCurrentFrame(), { parentOnly = true } )
	local data = {}
	local parametersLookup = parametersLookup
	
	if type( parametersLookup ) == 'table' then
		for k, _ in pairs( parametersLookup ) do
			data[k] = mw.text.trim(args[k])
		end
	end
	
	return data
end


function class:getPrintout( field, data )
	--[[
	the printout of field depends on certain things:
	* is it a table (aka list) or a single item
	* should it be linked (aka is it present in linkFields)
	* if so, should it be form linked? (aka not simply set to true but to a string)
	--]]
	
	local data = data
	
	if type( data ) == 'table' then
		local ret = {}
		for k, v in pairs( data ) do
			ret[k] = self:getPrintout( field, v )
		end
		return mw.text.listToText( ret, ', ', ' and ' )
	end
	
	if self.config.linkFields and type( self.config.linkFields ) == 'table' and self.config.linkFields[field] then
		if type( self.config.linkFields[field] ) == 'string' then
			local frame = mw.getCurrentFrame()
			local args = {
				form = self.config.linkFields[field],
				['link text'] = data,
				['existing page link text'] = data
			}
			return frame:callParserFunction{ name='#formredlink:target=' .. data, args=args }
		else
			return '[[' .. data .. ']]'
		end
	else
		return data
	end
end

function class:prepareData( data )
	local data = data
	local errorText

	data = self:setDefaults( data )
	
	data, errorText = self:checkForMandatoryFields( data, self.config.mandatory )
	
	data = self:convertListFields( data, self.config.listFields )
	
	return data, errorText
end

function class:retrieveClassData( selectionFilter )
	local selectionFilter = selectionFilter and ' ' .. selectionFilter or ''
	
	if not self.config.category then
		error ( 'No catergory found in class!' )
		return {}
	end
	
	-- build query
	local query = {
		'[[Category:' .. self.config.category .. ']]' .. selectionFilter,
		limit = 500,
		mainlabel = '-',
		'?#-=_mainlabel',
	}
	
	if self.config.parameters then
		for field, property in pairs( self.config.parameters ) do
			if property then
				table.insert( query, '?' .. property .. '#-=' .. field )
			end
		end
	else
		error( 'No parameter-to-property configuration found in class!' )
	end
	
	-- query is only returned to make the method easier to debug
	return mw.smw.ask( query ), query
end

function class:storeSemanticData( data, fieldsToPropertyLookup )
	local data = data
	local fieldsToPropertyLookup = fieldsToPropertyLookup or {}
	local delimiter = self.config.delimiter or ','
	local semanticData = {}
	
	if type( fieldsToPropertyLookup ) == 'table' then
		for field, property in pairs( fieldsToPropertyLookup ) do
			if data[field] and type( property ) == 'string' then
				if type( data[field] ) == 'table' then
					table.insert( semanticData, property .. '=' .. table.concat( data[field], delimiter ) )
					table.insert( semanticData, '+sep=' .. delimiter )
					--[[
					-- instead of using a delimiter with +set, you could do something like this:
					for _, v in pairs( data[field] ) do
						table.insert( semanticData, property .. '=' .. v )
					end
					--]]
				else
					table.insert( semanticData, property .. '=' .. data[field] )
				end
			end
		end
	end

	if #semanticData > 0 then
		mw.smw.set( semanticData )
	end
end


-- override when necessary
function class:setDefaults( data )
	local data = data
	return data
end

function class:alterDataAfterStorage( data )
	local data = data
	return data
end

-- return class
return class
Cookies help us deliver our services. By using our services, you agree to our use of cookies.