Module:SSC base
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:
- filter known and existant argument data from template arguments
- checks for existance of mandatory arguments
- converts all list fields into a table
- saves the semantic data
- builds a nice infobox (utilizing Module:Infobox)
- 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