Module:entity

From Rhizome Artbase

Documentation for this module may be created at Module:entity/doc

local p = {}
local m = require('Module:manifest')
local template = require('Module:entityTemplate')
local lib = require('Module:entityUtilities')

-- Test in console:
-- =p.render({ ['args'] = {'Q2508'} })

------------------------------------------------
-- dispatch function
function p.render(args)
	local arg_entity, d
	
	-- Called from a page as a template (frame) or from another function
	-- (regular args)?
	if args.args then
		arg_entity = lib.entityIdFromFrame(args)
		d = tonumber(args.args[2]) or 0
	else
		arg_entity = args[1]
		d = tonumber(args[2]) or 0
	end
	
	local entities = {}
	-- check if an entity ID, a list of entity IDs,
	-- an entity table or a list of entity tables
	-- needs to be handled.
	-- Final product is a list of entities.
	if type(arg_entity) == 'table' then
		if table.getn(arg_entity) == 0 then
			-- an empty list: nothing to render!
			-- return ''
		elseif lib.isEntity(arg_entity) then
			entities = {arg_entity}
		else
			for _,v in ipairs(arg_entity) do
				if lib.isEntity(v) then
					table.insert(entities, v)
				elseif mw.wikibase.isValidEntityId(v) then
					table.insert(entities, mw.wikibase.getEntity(v))
				else
					error('Given table should be an entity, list of entities, or list of entity IDs: ' .. mw.dumpObject(v))
				end
			end
		end
	elseif mw.wikibase.isValidEntityId(arg_entity) then
		table.insert(entities, lib.entity(arg_entity))
	else
		-- if the item in the array is neither a list of entities nor entity IDs,
		-- do nothing for now.
	end
	
	-- iterate over entities
	local strs_out = {}

	for _,entity in ipairs(entities) do
		local templateKey = p._findTemplate(entity)
		if templateKey then
			table.insert(strs_out, p['__' .. templateKey](entity, d))
		else
			table.insert(strs_out, string.format('No suitable function found to render entity %s. ', tostring(entity.id)))
		end
	end

	local separator = ''
	if d > 2 then
		separator = ', '
	end
	return table.concat(strs_out, separator)
end

-- check if there is a custom function to render an item
function p._findTemplate(entity)
	entity = lib.entity(entity)
	local found_fitting_template = false
	for _,statement in pairs( entity:getAllStatements(m.prop.instance_of) ) do
		if not found_fitting_template then
			local entity_instance_of = statement.mainsnak.datavalue.value.id
			for key,qid in pairs(m.item) do
				if entity_instance_of == qid and p['__' .. key] ~= nil then
					found_fitting_template = key
				end
			end
		end
	end
	return found_fitting_template
end

--------------------------------------------------------------------------------
-- Simple rendering functions for statements that contain values,
-- and entity properties


function p._instance_of_links(entity)
	entity = lib.entity(entity)
	local links = {}
	local variant_claims = lib.statementList{entity, m.prop.instance_of}
	for _,item in ipairs(variant_claims) do
		if lib.pageExists(item.id) then
			local target_link = mw.title.makeTitle(0, item.id)
			table.insert(links, template.render('instance_of_link', {label=item:getLabel(), target=target_link:fullUrl()}))
		else
			local descr = item:getDescription() or ''
			table.insert(links, template.render('instance_of_pill', {label=item:getLabel(), descr=descr}))
		end
	end
	local edit_target_link = mw.title.new('Item:' .. entity.id)
	table.insert(links, template.render('edit_link', {target=edit_target_link:fullUrl()}))
	return template.render('instance_of_links', {links=table.concat(links, ' ')})
end

function p._aliases(entity)
	local aliases = {}
	if entity.aliases then
		for lang,alias_list in pairs(entity.aliases) do
			for _,alias in pairs(alias_list) do
				table.insert(aliases, alias.value)
			end
		end
	end
	if #aliases == 0 then
		return template.HTML.none
	else
		return table.concat(aliases, ', ')
	end
end

function p._linkToEntities(entities)
	if type(entities) == 'string' or lib.isEntity(entities) then
		entities = {entities}
	end
	local links = {}
	for _,e in ipairs(entities) do
		if lib.entityIsUnknown(e) then
			table.insert(links, template.HTML.unknown)
		elseif lib.entityIsNoValue(e) then
			table.insert(links, template.HTML.none)
		else
			local e = lib.entity(e)
			local label = e:getLabel()
			local pageLink = e:getSitelink() or e.id
			table.insert(links, template.link(label, pageLink))
		end
	end
	return table.concat(links, ', ')
end

function p._url(statements)
	if lib.keyInTable(statements, 'mainsnak') then
		statements = {statements}
	end
	local urls = {}
	for _,statement in ipairs(statements) do
		table.insert(urls, mw.wikibase.formatValue(statement.mainsnak))
	end
	if #urls == 0 then
		return template.HTML.no_data
	else
		return table.concat(urls, ', ')
	end
end

function p._monolingualtext(statements)
	if lib.keyInTable(statements, 'mainsnak') then
		statements = {statements}
	end
	local strings = {}
	for _,statement in ipairs(statements) do
		table.insert(strings, mw.wikibase.formatValue(statement.mainsnak))
	end
	if #strings == 0 then
		return template.HTML.no_data
	else
		return table.concat(strings, ', ')
	end
end

function p._string(statements)
	if lib.keyInTable(statements, 'mainsnak') then
		statements = {statements}
	end
	local strings = {}
	for _,statement in ipairs(statements) do
		table.insert(strings, mw.wikibase.formatValue(statement.mainsnak))
	end
	if #strings == 0 then
		return template.HTML.no_data
	else
		return table.concat(strings, ', ')
	end
end

function p._pointInTime(statements)
	if lib.keyInTable(statements, 'mainsnak') then
		statements = {statements}
	end
	local pointsInTime = {}
	for _,statement in ipairs(statements) do
		table.insert(pointsInTime, mw.wikibase.formatValue(statement.mainsnak))
	end
	if table.getn(pointsInTime) == 0 then
		return template.HTML.no_data
	else
		return table.concat(pointsInTime, ', ')
	end
end

function p._pointInTimeYearOnly(statements)
	if lib.keyInTable(statements, 'mainsnak') then
		statements = {statements}
	end
	local pointsInTime = {}
	for _,statement in ipairs(statements) do
		table.insert(pointsInTime, string.sub(mw.wikibase.renderSnak(statement.mainsnak), -4))
	end
	if table.getn(pointsInTime) == 0 then
		return template.HTML.no_data
	else
		return table.concat(pointsInTime, ', ')
	end
end

function p._legacy_tags(statements)
	if lib.keyInTable(statements, 'mainsnak') then
		statements = {statements}
	end
	local legacy_tags = {}
	for _,statement in ipairs(statements) do
		local data = {}
		data.tags = statement.mainsnak.datavalue.value
		if statement.qualifiers then
			if statement.qualifiers[m.prop.attributed_to] then
				for __,qualifier in ipairs(statement.qualifiers[m.prop.attributed_to]) do
					data.attributed_to = p.render{qualifier.datavalue.value.id, 2}
				end
			end
		end
		table.insert(legacy_tags, template.render('legacy_tags_1', data))
	end
	local HTML = table.concat(legacy_tags, '')
	if #legacy_tags > 0 then
		HTML = template.render('legacy_tags', {tags=HTML})
		return HTML
	end
	return ''
end

function p._transcludeDocument(qid)
	local page_name = 'Document:'..qid
	if lib.pageExists(page_name) then
		local page = mw.title.new(page_name)
		local page_content = page:getContent()
		if lib.isEmpty(page_content) then
			return template.HTML.no_data
		else
			return page_content
		end
	else
		return template.HTML.no_data 
	end
end

--------------------------------------------------------------------------------
-- rendering a time period for an entity of start and end time (inception/conclusion)
-- are present. otherwise just show inception.

function p._time(entity)
	entity = lib.entity(entity)
	if entity.claims[m.prop.conclusion] then
		local conclusion = p._values(entity:getBestStatements(m.prop.conclusion))
		local inception = p._values(entity:getBestStatements(m.prop.inception))
		return template.render('active_period', {inception=inception, conclusion=conclusion})
	else
		return p._data_lines(entity, {m.prop.inception})
	end
end


--------------------------------------------------------------------------------
-- Rendering lists of values

function p._values(values)
	local results = {}
	for _,val in ipairs(values) do
		if val.mainsnak.datatype == 'time' then
			table.insert(results, p._pointInTime(val))
		elseif val.datatype == 'wikibase-item' then
			table.insert(results, p._linkToEntities(val))
		elseif val.datatype == 'string' then
			table.insert(results, p._string(val))
		elseif val.mainsnak.datatype == 'url' then
			table.insert(results, p._url(val))
		elseif val.mainsnak.datatype == 'monolingualtext' then
			table.insert(results, p._monolingualtext(val))
		end
	end
	return table.concat(results, ', ')
end


function p._data_lines(entity, properties)
	entity = lib.entity(entity)

	local data_lines = {}
	for _,property in ipairs(properties) do
		local statements = lib.statementList{entity, property}
		local prop_entity = lib.entity(property)

		-- check if the property should be rendered even if it has no data,
		-- see manifest's optional properties
		local render_property = true
		if #statements == 0 then
			local instance_of_ids = lib.instances_of(entity)
			for _,instance_of_id in ipairs(instance_of_ids) do
				if lib.keyInTable(m.prop_optional, instance_of_id) then
					for _,prop_optional in ipairs(m.prop_optional[instance_of_id]) do
						if prop_optional == property then
							render_property = false
						end
					end
				end
			end
		end

		if render_property then
			
			local label = prop_entity:getLabel()
			-- if several statements are rendered, check for plural form of
			-- the property's label:
			if #statements > 1 then
				if lib.keyInTable(prop_entity.claims, m.prop.plural_form_of_label) then
					label = prop_entity.claims[m.prop.plural_form_of_label][1].mainsnak.datavalue.value.text
				end
			end

			if prop_entity.datatype == 'time' then
				table.insert(data_lines, template.render('data_line', {
					pid=prop_entity.id,
					key=label, 
					value=p._pointInTime(statements)
				}))
			elseif prop_entity.datatype == 'wikibase-item' then
				local renderedValues = {}
				for _,statement in ipairs(statements) do
					local referencedEntity = lib.entity(statement)
					local templateKey = p._findTemplate(referencedEntity)
					if templateKey then
						table.insert(renderedValues, p.render{{referencedEntity}, 3})
					else
						table.insert(renderedValues, p._linkToEntities(statement))
					end
				end
				table.insert(data_lines, template.render('data_line', {
					pid=prop_entity.id,
					key=label, 
					value=table.concat(renderedValues, ', ')
				}))
			elseif prop_entity.datatype == 'string' then
				table.insert(data_lines, template.render('data_line', {
					pid=prop_entity.id,
					key=label, 
					value=p._string(statements)
				}))
			elseif prop_entity.datatype == 'url' then
				table.insert(data_lines, template.render('data_line', {
					pid=prop_entity.id,
					key=label, 
					value=p._url(statements)
				}))
			elseif prop_entity.datatype == 'monolingualtext' then
				table.insert(data_lines, template.render('data_line', {
					pid=prop_entity.id,
					key=label,
					value=p._monolingualtext(statements)
				}))
			else
				table.insert(data_lines, 'No function to render statements of data type ' .. prop_entity.datatype)
			end
		end
	end

	return table.concat(data_lines)
end

function p._data_line_nested(entity, nesting_property, collect_property)
	local prop_entity = lib.entity(collect_property)

	local statements = lib.nestedStatementList(entity, nesting_property, collect_property)

	local local_statements = lib.statementList{entity, collect_property}
	for _,v in ipairs(local_statements) do
		table.insert(statements, v)
	end

	local values = p._values(statements)

	local label = prop_entity:getLabel()
	if #statements > 1 then
		if lib.keyInTable(prop_entity.claims, m.prop.plural_form_of_label) then
			label = prop_entity.claims[m.prop.plural_form_of_label][1].mainsnak.datavalue.value.text
		end
	end
	if #statements > 0 then
		return template.render('data_line', {pid=prop_entity.id, key=label, value=values})
	else
		return ''
	end
end

--------------------------------------------------------------------------------
-- Complex sub-components


function p._carousel(entity)
	local entity = lib.entity(entity)
	
	local carousel_context = {}
	carousel_context.carousel_id = 'Carousel' .. entity.id
	
	local images_strs_out = {}
	
	local images = lib.statementList{entity, m.prop.image}
	
	local active = ' active'
	
	if table.getn(images) > 0 then
		for _,image in ipairs(images) do
			if image.mainsnak.datatype == 'localMedia' then
				local image_context = {}
				image_context.active = active
				image_context.image_name = image.mainsnak.datavalue.value
				image_context.image_page = mw.title.new('File:' .. image_context.image_name):canonicalUrl()
				image_context.image_url = mw.title.new('Special:FilePath/' .. image_context.image_name):canonicalUrl()
				if image_context.image_url:match"\.gif$" then
					table.insert(images_strs_out, template.render('carousel_image_gif', image_context))
				else
					table.insert(images_strs_out, template.render('carousel_image', image_context))
				end
				active = ''
			end
		end
	end
	if table.getn(images) > 1 then
		carousel_context.carousel_controls = template.render('carousel_controls', {carousel_id = carousel_context.carousel_id})
	else
		carousel_context.carousel_controls = ''
	end
	
	carousel_context.carousel_images = table.concat(images_strs_out, '')
	
	return template.render('carousel', carousel_context)
end

--------------------------------------------------------------------------------
-- Artwork item

function p.__artwork(entity, d)
	local data = {}
	data.label = entity:getLabel()
	data.qid = entity.id
	
	if d == 0 then	
		-- instance of
		data.instance_of = p._instance_of_links(entity)

		-- credits (artist + production year)
		data.credits = p._data_lines(entity, {m.prop.artist})
		data.credits = data.credits .. p._time(entity)
		
		-- image carousel
		data.carousel = p._carousel(entity)
		
		-- get descriptions (summaries always on top)
		local summary = lib.statementList{entity, m.prop.description, filter=m.item.summary, order_by=m.prop.inception, desc=true}
		summary = p.render{summary, 1}

		local description = lib.statementList{entity, m.prop.description, filter=m.item.description, order_by=m.prop.inception, desc=true}
		description = p.render{description, 1}

		local artist_statement = lib.statementList{entity, m.prop.description, filter=m.item.artist_statement, order_by=m.prop.inception, desc=true}
		artist_statement = p.render{artist_statement, 1}

		data.description = summary .. description .. artist_statement
		
		-- get variants
		local variants = lib.statementList{entity, m.prop.variant, order_by=m.prop.inception}
		data.variant_links = p.render{variants, 2}
		
		-- legacy tags
		data.legacy_tags = lib.statementList{entity, m.prop.legacy_tags}
		data.legacy_tags = p._legacy_tags(data.legacy_tags)
		
		-- metadata
		---- descriptive
		data.data_lines = p._data_lines(entity, {m.prop.artist}) ..
						  template.render('data_line', {key='title', value=data.label}) ..
						  p._data_lines(entity, {m.prop.inception})
		local metadata_descriptive = template.render('metadata_descriptive', data)
		
		--- variant_history
		local metadata_variant_history = ''
		if #variants > 0 then
			local variants_rendered = p.render{variants, 1}
			metadata_variant_history = template.render('variant_history', {variants=variants_rendered})
		end

		--- administrative
		local license = p._data_lines(entity, {m.prop.license})
		local alternative_titles = p._data_line_nested(entity, m.prop.variant, m.prop.alternative_title)

		local administrative_data_lines = alternative_titles .. license
		
		local metadata_administrative = ''
		if string.len(administrative_data_lines) > 0 then
			metadata_administrative = template.render('metadata_administrative', {data_lines=administrative_data_lines})
		end

		data.metadata = metadata_descriptive .. metadata_variant_history .. metadata_administrative
		
		
		-- Set page display title to artwork title
		mw.ext.displaytitle.set(data.label)
		
		
		return template.render('artwork_0', data)
	
	elseif d == 1 then

		data.inception = p._pointInTimeYearOnly(entity.claims[m.prop.inception])
		
		local artists = lib.statementList{entity, m.prop.artist}
		data.artist = p.render{artists, 4}

		data.image_url = 'pikachu.png'

		data.artwork_page_url = mw.title.new(entity.id):canonicalUrl()

		-- look for preview image and use that if possible, if none exits pick 
		-- first one from regular images
		local images
		if entity.claims[m.prop.preview_image] then
			images = mw.wikibase.getBestStatements(entity.id, m.prop.preview_image)
		else
			images = mw.wikibase.getBestStatements(entity.id, m.prop.image)
		end

		if #images > 0 then
			local topimage = images[1]
			if topimage.mainsnak.datatype == 'localMedia' then
				local image_context = {}
				local image_name = topimage.mainsnak.datavalue.value
				data.image_url = mw.title.new('Special:Redirect/file/' .. image_name):canonicalUrl()
				if not data.image_url:match"\.gif$" then
					data.image_url = data.image_url .. '?width=800'
				end
			end
		end

		return template.render('artwork_1', data)

	elseif d == 2 or d == 3 then
		return p._linkToEntities(entity)
	elseif d == 4 then
		return data.label
	end
	
	return string.format('No support for rendering entity of type artwork at distance level %s.', tostring(d))
end

--------------------------------------------------------------------------------
-- Person item

function p.__person(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.label = entity:getLabel()
	data.qid = entity.id
	
	if d == 0 then

		data.instance_of = p._instance_of_links(entity)

		data.data_lines = p._data_lines(entity, {m.prop.official_website, m.prop.member_of})

		local artworks = lib.statementList{entity, m.prop.artwork, filter=m.item.artwork, order_by=m.prop.inception}
		
		data.artworks = ''
		if #artworks > 0 then
			data.artworks = template.render('person_0_artworks', {artworks=p.render{artworks, 1}})
		end

		-- Set page display title to person name
		mw.ext.displaytitle.set(data.label)

		return template.render('person_0', data)

	elseif d == 2 or d == 3 then
		return p._linkToEntities(entity)
		
	elseif d == 4 then
		return data.label
	
	end
	
	return string.format('No support for rendering entity of type person at distance level %s.', tostring(d))
end 


--------------------------------------------------------------------------------
-- collective item

function p.__collective(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.label = entity:getLabel()
	data.qid = entity.id
	
	if d == 0 then

		data.instance_of = p._instance_of_links(entity)

		data.data_lines = p._data_lines(entity, {m.prop.has_member, m.prop.official_website})

		local artworks = lib.statementList{entity, m.prop.artwork, filter=m.item.artwork, order_by=m.prop.inception}
		data.artworks = p.render{artworks, 1}

		mw.ext.displaytitle.set(data.label)

		return template.render('collective_0', data)

	elseif d == 2 or d == 3 then
		local members = lib.statementList{entity, m.prop.has_member}
		local members_rendered = {}
		for _,member in ipairs(members) do
			table.insert(members_rendered, p.render{{member}, d+1})
		end
		data.members = table.concat(members_rendered, ', ')
		if #data.members > 0 then
			data.members = ' (' .. data.members .. ')'
		end
		data.collective_link = p._linkToEntities(entity)
		return template.render('collective_1', data)
			
	elseif d == 4 then
		return data.label
	end
	
	return string.format('No support for rendering entity of type collective at distance level %s.', tostring(d))
end 


--------------------------------------------------------------------------------
-- Institution item

function p.__institution(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.label = entity:getLabel()
	data.qid = entity.id
	
	if d == 2 or d == 3 then
		return p._linkToEntities(entity)
	
	elseif d == 4 then
		return data.label
	
	end
	
	return string.format('No support for rendering entity of type institution at distance level %s.', tostring(d))
end 

--------------------------------------------------------------------------------
-- Description item

function p.__description(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.id = entity.id
	data.instance_of_links = p._instance_of_links(entity)
	
	if d == 1 then
		
		data.document = p._transcludeDocument(data.id)

		if data.document == template.HTML.no_data then
			-- skip empty or non-existing pages
			return ''
		else
			data.data_lines = p._data_lines(entity, {	m.prop.attributed_to,
														m.prop.inception,
														m.prop.generated_by,
														m.prop.copyedited_by})
			
			return template.render('description_1', data)
		end
	end	
	
	return string.format('No support for rendering entity of type description at distance level %s.', tostring(d))
end

--------------------------------------------------------------------------------
-- Summary item (almost same as description)

function p.__summary(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.id = entity.id
	data.instance_of_links = p._instance_of_links(entity)
	
	if d == 1 then
		
		data.document = p._transcludeDocument(data.id)

		if data.document == template.HTML.no_data then
			-- skip empty or non-existing pages
			return ''
		else

			data.data_lines = p._data_lines(entity, {	m.prop.attributed_to,
														m.prop.inception,
														m.prop.generated_by,
														m.prop.copyedited_by})
			
			return template.render('summary_1', data)
		end
	end	
	
	return string.format('No support for rendering entity of type summary at distance level %s.', tostring(d))
end
--------------------------------------------------------------------------------
-- artist statement item

function p.__artist_statement(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.id = entity.id
	data.instance_of_links = p._instance_of_links(entity)
	
	if d == 1 then
		
		data.document = p._transcludeDocument(data.id)

		if data.document == template.HTML.no_data then
			-- skip empty or non-existing pages
			return ''
		else
			data.data_lines = p._data_lines(entity, {	m.prop.attributed_to,
														m.prop.inception,
														m.prop.generated_by,
														m.prop.copyedited_by})
			
			return template.render('description_1', data)
		end
	end	
	
	return string.format('No support for rendering entity of type description at distance level %s.', tostring(d))
end


--------------------------------------------------------------------------------
-- Variant item

function p.__variant(entity, d)
	local entity = lib.entity(entity)
	
	local data = {}
	data.qid = entity.id
	
	data.access_url = entity:formatPropertyValues(m.prop.access_url, {
						mw.wikibase.entity.claimRanks.RANK_NORMAL, 
		                mw.wikibase.entity.claimRanks.RANK_PREFERRED }
		              )['value']
		                                                   
	data.type_of_artifact = template.HTML.unknown
	local artifacts = lib.statementList{entity, m.prop.artifact} -- , filter=m.item.type_of_artifact}

	-- this needs to be expanded to handle multiple artifact types per variant!
	if #artifacts > 0 then
		local types_of_artifact = lib.statementList{artifacts[1], m.prop.instance_of, filter=m.item.type_of_artifact}
		if #types_of_artifact > 0 then
			data.type_of_artifact = types_of_artifact[1]:getLabel()
			data.instance_of = types_of_artifact[1].id
		end
	end
	
	if d == 1 then
		
		data.variant_id = entity:getId()
		
		data.inception = lib.statementList{entity, m.prop.inception}
		data.inception = p._pointInTime(data.inception)

		local instance_of_links = p._instance_of_links(entity)

		local data_lines = p._data_lines(entity, {	m.prop.not_active_since,
													m.prop.access_url,
													m.prop.alternative_title,
													m.prop.inception,
													m.prop.conclusion,
													m.prop.generated_by,
													m.prop.derived_from,
													m.prop.type_of_accession,
													m.prop.date_of_accession,
													m.prop.associated_with,
													m.prop.in_collection_of})


		data.data_lines = instance_of_links .. data_lines
		
		return template.render('variant_1', data)
		
	elseif d == 2 then
		-- do not display access links for variants that
		-- are known to not be active anymore (dead link)
		if entity.claims[m.prop.not_active_since] then
			return ''
		else
			if data.type_of_artifact ~= 'outside link' then
				data.variant_type = 'ArtBase variant'
			end
			return template.render('variant_2', data)
		end
	
	elseif d == 3 then
		return entity:getLabel()
	end
	
	
	
	return string.format('No support for rendering entity of type variant at distance level %s.', tostring(d))
end


--------------------------------------------------------------------------------


return p