Module:WikiJournal

From Wikiversity
Jump to navigation Jump to search

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

local p = {}

--[[
Function imported from https://en.wikipedia.org/w/index.php?title=Module:Time&oldid=958665680#L-232

decode ISO formatted date/time into a table suitable for os.time().
--]]
local function parseISODate(isoDate)
    if isoDate == nil then
        return nil
    end

    -- Wikibase
    if type(isoDate) == 'table' then
        if isoDate.datavalue ~= nil then
            isoDate = isoDate.datavalue.value.time
        end

        if isoDate.mainsnak ~= nil then
            isoDate = isoDate.mainsnak.datavalue.value.time
        end
    end

    local year, month, day, hour, minute, second;

    year, month, day, hour, minute, second = isoDate:match('(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)');
    if not year then
        year, month, day, hour, minute, second = isoDate:match('^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)$');
        if not year then
            return nil;
        end
    end

    return { ['year'] = year, ['month'] = month, ['day'] = day, ['hour'] = hour, ['min'] = minute, ['sec'] = second };
end

--[[
Iterates through a table of 'significant event'
Stops on the first 'submission' event found and returns the event data
]]--
local function getSubmissionFromEvents(events)
    events = events or {}

    for _, event in pairs(events) do
        if mw.wikibase.getLabel(event.mainsnak.datavalue.value.id) == 'submission' then
            return event.qualifiers['P585'][1]
        end
    end

    return nil
end

--[[
From a table of wikibase time snaks, returns either the lowest or highest date
]]--
local function getMaxMinDate(time, returnType)
    local max = -math.huge
    local min = math.huge

    time = time or {}

    for _, entry in pairs(time) do
        local success, timestamp = pcall(os.time, parseISODate(entry.datavalue.value.time))
        if success then
            if timestamp > max then
                max = timestamp
            end

            if timestamp < min then
                min = timestamp
            end
        else
            return false
        end
    end

    if returnType == 'max' then
        return max
    end

    return min
end

--[[
Loads the affiliations for an author with the given id
Returns only affiliations that start before a given publication date
or end after a given submission date

Arguments submission and publication are expected to be timestamps
]]--
local function getAuthorAffiliations(id, submission, publication, affiliationMap)
    if not id or type(id) ~= 'string' then
        return {}
    end

    -- load affiliations (P108 = Employer)
    local affiliations = mw.wikibase.getBestStatements(id, 'P108')

    -- Affiliations that end after submission
    -- Affiliations that start before publication
    local validAffiliations = {}

    for _, affiliation in pairs(affiliations) do
        affiliation.qualifiers = affiliation.qualifiers or {}

        local name
        -- Add the affiliation to the list of all affiliations
        if affiliation.mainsnak and affiliation.mainsnak.datatype == 'wikibase-item' then
            name = mw.wikibase.renderSnak(affiliation.mainsnak)
            if affiliation.qualifiers['P6424'] ~= nil then
                name = mw.wikibase.renderSnak(affiliation.qualifiers['P6424'][1])
            end

            affiliationMap[mw.hash.hashValue('md5', name)] = name
        end

        local startTime = affiliation.qualifiers['P580']
        local endTime = affiliation.qualifiers['P582']

        local startValid = false
        local endValid = false

        if startTime ~= nil and type(publication) == 'number' then
            -- If P580 (start time) occurs multiple times, we'll use the earliest
            local timestamp = getMaxMinDate(startTime, 'min')

            -- Diff time returns seconds from a to b = b - a
            -- If the difference from start time to publication is greater than 0
            -- the affiliation has started before the publication
            -- Timeline: > ---- |Timestamp| ---- |Publication| ---- >
            startValid = timestamp ~= false and os.difftime(publication, timestamp) >= 0
        end

        if endTime ~= nil and type( submission ) == 'number' then
            -- If P582 (end time) occurs multiple times, we'll use the latest
            local timestamp = getMaxMinDate(endTime, 'max')

            -- Diff time returns seconds from a to b = b - a
            -- If difference the from submission to the end time is greater than 0
            -- the affiliation ends after the submission
            -- Timeline: > ---- |Submission| ---- |Timestamp| ---- >
            endValid = timestamp ~= false and os.difftime(timestamp, submission) >= 0
        end

        if startTime ~= nil and endTime ~= nil then
            -- Insert only if both are true
            if startValid and endValid then
                table.insert(validAffiliations, affiliation)
            end
        else
            -- Insert if either one is true
            if startValid or endValid then
                table.insert(validAffiliations, affiliation)
            end
        end
    end

    return validAffiliations
end


--[[
Iterates through a list of affiliations, saves the affiliation by its md5 hashed name
Adds the affiliation hash to the author
]]--
local function addAffiliations(affiliationData, allAffiliations, auths, ordinal)
    if type(affiliationData) ~= 'table' then
        return nil
    end

    for _, affiliation in pairs(affiliationData) do
        if affiliation.mainsnak and affiliation.mainsnak.datatype == 'wikibase-item' then
            if affiliation.qualifiers and affiliation.qualifiers['P6424'] ~= nil then
                affiliation = affiliation.qualifiers['P6424'][1]
            else
                affiliation = affiliation.mainsnak
            end
        end

        local name = mw.wikibase.renderSnak(affiliation)

        -- Key is the MD5 value of the affiliation name
        -- We can't use affiliation.hash as mainsnaks do not contain such a key
        local affiliationKey = mw.hash.hashValue('md5', name)

        if not allAffiliations[affiliationKey] then
            allAffiliations[affiliationKey] = name
        end

        if type(auths[ordinal].affiliation) ~= 'table' then
            auths[ordinal].affiliation = {}
        end

        -- 'Set' like behaviour, does not add duplicate keys
        auths[ordinal].affiliation[affiliationKey] = true
    end
end

--[[
Iterates through a list of qualifiers and adds wanted properties to the author table
]]--
local function processQualifiers( qualifiers, affiliations, auths, ordinal)
    for id, data in pairs( qualifiers ) do
        -- E-Mail qualifier
        if id == 'P968' then
            local email = mw.wikibase.renderSnak( data[1] )
            local split = mw.text.split( email, ':', true )

            -- Remove 'mailto:' part
            if #split == 2 then
                auths[ordinal].email = split[2]
            else
                auths[ordinal].email = email
            end
        end

        -- affiliation / affiliation_string
        if id == 'P1416' or id == 'P6424' then
            addAffiliations(data, affiliations, auths, ordinal)
        end
    end
end

-- Return the entity ID of the item linked to the current page.
function p.QID(frame)
	if not mw.wikibase then
		return "no mw.wikibase"
	end
	return mw.wikibase.getEntityIdForTitle( mw.title.getCurrentTitle().subjectPageTitle.text ) or ""
end


--[[
Fetch info from Wikidata and format lists

Author info (name, employer, orcid)
--]]

function p.getAuthors(frame)
	local args= frame.args
	if not args.qid and not args[1] then
		args = frame:getParent().args
	end

	local qid = mw.text.trim(args[1] or args.qid or "")
	if qid == "" then qid = mw.wikibase.getEntityIdForCurrentPage() end
	if not qid then return end

    -- qualID is a string list of wanted qualifiers or "ALL"
	local qualID = mw.text.trim(args.qual or ""):upper()
    local allflag = (qualID == "ALL")
    -- create table of wanted qualifiers as key
    local qwanted = {}
    -- create sequence of wanted qualifiers
    local qorder = {}
    for q in mw.text.gsplit(qualID, "%p") do -- split at punctuation and iterate
        local qtrim = mw.text.trim(q)
        if qtrim ~= "" then
            qwanted[mw.text.trim(q)] = true
            qorder[#qorder+1] = qtrim
        end
    end

    -- Returns all wanted qualifiers as a comma separated string
    local function renderQualifiers(qualifiers)
        if type(qualifiers) ~= 'table' then
            return ''
        end

        if type(qorder) ~= 'table' then
            qorder = {}
        end

        -- get author string's qualifiers if wanted
        local qtbl = {}
        local qualtxt = ""
        if qualifiers and (allflag or #qorder > 0) then
            for prop, val in pairs(qualifiers) do
                -- TODO REMOVE P968 (E-Mail) IN TEMPLATE
                if allflag or (qwanted[prop] and prop ~= 'P968') then
                    -- render the first value of each qualifier
                    qtbl[#qtbl + 1] = mw.wikibase.renderSnak(val[1])
                end
            end

            qualtxt = table.concat(qtbl, ", ") -- use comma space separators
        end

        return qualtxt
    end

	-- construct a table of tables:
	-- key = series ordinal;
	-- value = {name=author's name, orcid=author's orcid id, emp=author's employer. quals = qualifiers}
	local auths = {}
	-- series ordinal = true when used
	local ords = {}
    -- list of affiliations
    local affiliations = {}
	-- keep track of values without series ordinals and max ordinal
	local noord = 0
	local maxord = 0

    -- Timestamp of the article submission or nil
    local articleSubmission = getSubmissionFromEvents(mw.wikibase.getBestStatements(qid, 'P793'))
    -- Timestamp of the article publication or nil
    local articlePublication = mw.wikibase.getBestStatements(qid, 'P577')[1]
    _, articleSubmission = pcall(os.time, parseISODate(articleSubmission))
    _, articlePublication = pcall(os.time, parseISODate(articlePublication))

    -- get authors that have entries
	local prop50 = mw.wikibase.getBestStatements(qid, "P50")
	for i, v in ipairs(prop50) do
		if v.mainsnak.snaktype == "value" then
			-- get author's qid
			local nameid = v.mainsnak.datavalue.value.id

			-- get author's name
			local name = mw.wikibase.getLabel(nameid) or "No label"

			-- get author's orcid id
			local orcid
			local orcidtbl = mw.wikibase.getBestStatements(nameid, "P496")[1]
			if orcidtbl and orcidtbl.mainsnak.snaktype == "value" then
				orcid = orcidtbl.mainsnak.datavalue.value
			end

			-- get ordinal
			local ordinal = noord
			if v.qualifiers then
				local qualP1545 = v.qualifiers["P1545"][1]
				if qualP1545 then
					ordinal = tonumber(qualP1545.datavalue.value) or noord
				end
			end
			if ordinal == noord then noord = noord -1 end
			-- check for a duplicate ordinal
			if ords[ordinal] then
				repeat ordinal = ordinal + 1 until not ords[ordinal]
			end

            local qualtxt = renderQualifiers(v.qualifiers, allflag, qorder, qwanted)

			auths[ordinal] = {name = name, orcid = orcid, emp = emp, quals = qualtxt}
			ords[ordinal] = true

            -- Add affiliation and email to the author
            processQualifiers(v.qualifiers, affiliations, auths, ordinal)

            -- If no affiliation was added, load it from the authors wikibase page
            if auths[ordinal].affiliation == nil then
                local validAffiliations = getAuthorAffiliations(nameid, articleSubmission, articlePublication, affiliations)

                if #validAffiliations > 0 then
                    addAffiliations(validAffiliations, affiliations, auths, ordinal)
                end
            end

			if ordinal > maxord then maxord = ordinal end
		end
	end

	-- add author's name strings to the author's table
	local propP2093 = mw.wikibase.getBestStatements(qid, "P2093")
	for i, v in ipairs(propP2093) do
		if v.mainsnak.snaktype == "value" then
			local name = v.mainsnak.datavalue.value
			local ordinal = noord
			if v.qualifiers then
				local qualP1545 = v.qualifiers["P1545"][1]
				if qualP1545 then
					ordinal = tonumber(qualP1545.datavalue.value) or noord
				end
			end

            local qualtxt = renderQualifiers(v.qualifiers, allflag, qorder, qwanted)

			-- get ordinal
			if ordinal == noord then noord = noord -1 end
			-- check for a duplicate ordinal
			if ords[ordinal] then
				repeat ordinal = ordinal + 1 until not ords[ordinal]
			end
			auths[ordinal] = {name = name, quals = qualtxt}
			ords[ordinal] = true

            -- Add affiliation and email to the author
            processQualifiers(v.qualifiers, affiliations, auths, ordinal)

			if ordinal > maxord then maxord = ordinal end
		end
	end

	-- compact the auths table into a sequence
	local authors = {}
	for idx = 1, maxord do
		if auths[idx] then table.insert(authors, auths[idx]) end
	end
	for idx = 0, noord, -1 do
		if auths[idx] then table.insert(authors, auths[idx]) end
	end

    -- Maps an affiliation hash to the first author that used it
    local affiliationsUsed = {}

	-- construct some output
	local out = {}

	for i, v in ipairs(authors) do
		out[i] = v.name

        if type(v.affiliation) == 'table' then
            for hash, _ in pairs(v.affiliation) do
                if affiliationsUsed[hash] == nil then
                    out[i] = out[i] .. frame:expandTemplate{ title = 'efn', args = { name=hash , affiliations[hash] } }
                else
                    -- Affiliation was used, make a reference
                    out[i] = out[i] .. frame:callParserFunction{ name = '#tag:ref', args = {
                        affiliations[hash],
                        name = hash,
                        group = 'lower-alpha'
                    } }
                end
            end
        end

        if v.email then
            out[i] = out[i] .. frame:expandTemplate{ title = 'efn-lr', args = { name=v.email , v.email } }
        end

		if v.orcid then
			out[i] = out[i] .. " [[file:ORCID_iD.svg|frameless|text-bottom|16px|link=https://orcid.org/" .. v.orcid .. " ]]"
		end

		if v.quals ~= "" then
            out[i] = out[i] .. frame:expandTemplate{ title = 'efn-ua', args = { name=v.quals , v.quals } }
		end
	end

	-- glue the list of authors together as a comma separated list
	local returntxt = table.concat(out, ", ")
	return returntxt
end

--[[
Author plain info (name only)
--]]

function p.getAuthorsPlain(frame)
	local args= frame.args
	if not args.qid and not args[1] then
		args = frame:getParent().args
	end

	local qid = mw.text.trim(args[1] or args.qid or "")
	if qid == "" then qid = mw.wikibase.getEntityIdForCurrentPage() end
	if not qid then return end

	-- construct a table of tables:
	-- key = series ordinal;
	-- value = {name=author's name}
	local auths = {}
	-- series ordinal = true when used
	local ords = {}
	-- keep track of values without series ordinals and max ordinal
	local noord = 0
	local maxord = 0
	-- get authors that have entries
	local prop50 = mw.wikibase.getBestStatements(qid, "P50")
	for i, v in ipairs(prop50) do
		if v.mainsnak.snaktype == "value" then
			-- get author's qid
			local nameid = v.mainsnak.datavalue.value.id
			-- get author's name
			local name = mw.wikibase.getLabel(nameid) or "No label"
			-- get ordinal
			local ordinal = noord
			if ordinal == noord then noord = noord -1 end
			-- check for a duplicate ordinal
			if ords[ordinal] then
				repeat ordinal = ordinal + 1 until not ords[ordinal]
			end
			auths[ordinal] = {name = name}
			ords[ordinal] = true
			if ordinal > maxord then maxord = ordinal end
		end
	end

	-- add author's name strings to the author's table
	local propP2093 = mw.wikibase.getBestStatements(qid, "P2093")
	for i, v in ipairs(propP2093) do
		if v.mainsnak.snaktype == "value" then
			local name = v.mainsnak.datavalue.value
			local ordinal = noord
			-- get ordinal
			if ordinal == noord then noord = noord -1 end
			-- check for a duplicate ordinal
			if ords[ordinal] then
				repeat ordinal = ordinal + 1 until not ords[ordinal]
			end
			auths[ordinal] = {name = name}
			ords[ordinal] = true
			if ordinal > maxord then maxord = ordinal end
		end
	end

	-- compact the auths table into a sequence
	local authors = {}
	for idx = 1, maxord do
		if auths[idx] then table.insert(authors, auths[idx]) end
	end
	for idx = 0, noord, -1 do
		if auths[idx] then table.insert(authors, auths[idx]) end
	end

	-- construct some output
	local out = {}
	for i, v in ipairs(authors) do
		out[i] = v.name
	end
	-- glue the list of authors together as a comma separated list
	local returntxt = table.concat(out, ", ")
	return returntxt
end



--[[
Editor info (name, role)
--]]

function p.getEditors(frame)
	local args= frame.args
	if not args.qid and not args[1] then
		args = frame:getParent().args
	end

	local qid = mw.text.trim(args[1] or args.qid or "")
	if qid == "" then qid = mw.wikibase.getEntityIdForCurrentPage() end
	if not qid then return end

    -- qualID is a string list of wanted qualifiers or "ALL"
	local qualID = mw.text.trim(args.qual or ""):upper()
    local allflag = (qualID == "ALL")
    -- create table of wanted qualifiers as key
    local qwanted = {}
    -- create sequence of wanted qualifiers
    local qorder = {}
    for q in mw.text.gsplit(qualID, "%p") do -- split at punctuation and iterate
        local qtrim = mw.text.trim(q)
        if qtrim ~= "" then
            qwanted[mw.text.trim(q)] = true
            qorder[#qorder+1] = qtrim
        end
    end

	-- construct a table of tables:
	-- key = series ordinal;
	-- value = {name=editor's name, orcid=editor's orcid id, quals=qualtxt}
	local eds = {}
	-- series ordinal = true when used
	local ords = {}
	-- list of roles
	local roles = {}
	-- keep track of values without series ordinals and max ordinal
	local noord = 0
	local maxord = 0
	-- get editors that have entries
	local prop98 = mw.wikibase.getBestStatements(qid, "P98")
	for i, v in ipairs(prop98) do
		if v.mainsnak.snaktype == "value" then
			-- get editor's qid
			local nameid = v.mainsnak.datavalue.value.id
			-- get editor's name
			local name = mw.wikibase.getLabel(nameid) or "No label"
			-- get editor's orcid id
			local orcid
			local orcidtbl = mw.wikibase.getBestStatements(nameid, "P496")[1]
			if orcidtbl and orcidtbl.mainsnak.snaktype == "value" then
				orcid = orcidtbl.mainsnak.datavalue.value
			end
			-- get editor's username
			local username
			local usernametbl = mw.wikibase.getBestStatements(nameid, "P4174")[1]
			if usernametbl and usernametbl.mainsnak.snaktype == "value" then
				username = usernametbl.mainsnak.datavalue.value
			end
			-- get editor's qualifiers if wanted
			local qtbl = {}
			local qualtxt = ""
			if v.qualifiers and (allflag or #qorder > 0) then
				for prop, val in pairs(v.qualifiers) do
					if allflag or qwanted[prop] then
						-- render the first value of each qualifier
						qtbl[#qtbl + 1] = mw.wikibase.renderSnak(val[1])
					end
				end
				qualtxt = table.concat(qtbl, ", ") -- use comma space separators
			end
			-- get ordinal
			local ordinal = noord
			if ordinal == noord then noord = noord -1 end
			-- check for a duplicate ordinal
			if ords[ordinal] then
				repeat ordinal = ordinal + 1 until not ords[ordinal]
			end
			eds[ordinal] = {name = name, orcid = orcid, quals = qualtxt, username = username}
			ords[ordinal] = true
			if ordinal > maxord then maxord = ordinal end
		end
	end

	-- compact the eds table into a sequence
	local editors = {}
	for idx = 1, maxord do
		if eds[idx] then table.insert(editors, eds[idx]) end
	end
	for idx = 0, noord, -1 do
		if eds[idx] then table.insert(editors, eds[idx]) end
	end

	-- construct some output
	local out = {}
	for i, v in ipairs(editors) do
		out[i] = v.name
		if v.orcid then
			out[i] = out[i] .. " [[file:ORCID_iD.svg|frameless|text-bottom|16px|link=https://orcid.org/" .. v.orcid .. " ]]"
		end
		if v.role then
			out[i] = out[i] .. " (" .. v.orcid .. ")"
		end
		if v.quals ~= "" then
			out[i] = out[i] .. " <small>(" .. v.quals ..")</small>"
		end
		if v.username then
			out[i] = out[i] .. " <small>[[Special:EmailUser/" .. v.username .. "|contact]]</small>"
		end
	end
	-- glue the list of editors together as a vertical list
	local returntxt = "<p>" .. table.concat(out, "<br>") .. "</p>"
	return returntxt
end


--[[
Reviewer info (name, orcid)
--]]

function p.getReviewers(frame)
	local args= frame.args
	if not args.qid and not args[1] then
		args = frame:getParent().args
	end

	local qid = mw.text.trim(args[1] or args.qid or "")
	if qid == "" then qid = mw.wikibase.getEntityIdForCurrentPage() end
	if not qid then return end

	-- construct a table of tables:
	-- key = series ordinal;
	-- value = {name=reviewer's name, orcid=reviewer's orcid id, role=reviewer's role}
	local eds = {}
	-- series ordinal = true when used
	local ords = {}
	-- list of roles
	local roles = {}
	-- keep track of values without series ordinals and max ordinal
	local noord = 0
	local maxord = 0
	-- get reviewers that have entries
	local prop4032 = mw.wikibase.getBestStatements(qid, "P4032")
	for i, v in ipairs(prop4032) do
		if v.mainsnak.snaktype == "value" then
			-- get reviewer's qid
			local nameid = v.mainsnak.datavalue.value.id
			-- get reviewer's name
			local name = mw.wikibase.getLabel(nameid) or "No label"
			-- get reviewer's orcid id
			local orcid
			local orcidtbl = mw.wikibase.getBestStatements(nameid, "P496")[1]
			if orcidtbl and orcidtbl.mainsnak.snaktype == "value" then
				orcid = orcidtbl.mainsnak.datavalue.value
			end
			-- get ordinal
			local ordinal = noord
			if ordinal == noord then noord = noord -1 end
			-- check for a duplicate ordinal
			if ords[ordinal] then
				repeat ordinal = ordinal + 1 until not ords[ordinal]
			end
			eds[ordinal] = {name = name, orcid = orcid}
			ords[ordinal] = true
			if ordinal > maxord then maxord = ordinal end
		end
	end

	-- compact the eds table into a sequence
	local reviewers = {}
	for idx = 1, maxord do
		if eds[idx] then table.insert(reviewers, eds[idx]) end
	end
	for idx = 0, noord, -1 do
		if eds[idx] then table.insert(reviewers, eds[idx]) end
	end

	-- construct some output
	local out = {}
	for i, v in ipairs(reviewers) do
		out[i] = v.name
		if v.orcid then
			out[i] = out[i] .. " [[file:ORCID_iD.svg|frameless|text-bottom|16px|link=https://orcid.org/" .. v.orcid .. " ]]"
		end
	end
	-- glue the list of reviewers together as a vertical list
	local returntxt = "<p>" .. table.concat(out, "<br>") .. "</p>"
	return returntxt
end

--[[
Extract fig from article (page, fig_number)
--]]

local Transcluder = require('Module:Transcluder')

-- Helper function to handle errors
function getError(message, value)
	if type(message) == 'string' then
		message = Transcluder.getError(message, value)
	end
	return message
end

function p.getFig(frame)
	local args = Transcluder.parseArgs(frame)

	-- Make sure the requested page exists
	local page = args[1]
	if not page then return getError('no-page') end
	local title = mw.title.new(page)
	if not title then return getError('no-page') end
	if title.isRedirect then title = title.redirectTarget end
	if not title.exists then return getError('page-not-found', page) end
	page = title.prefixedText

	-- Get the nth fig file
	local ok, figs = pcall(Transcluder.get, page, { only = 'templates', templates = 'fig' })
	if ok then
		local fig = Transcluder.getTemplates(figs, args.fig_number)[1]

		if fig ~= nil then
			local parameters = Transcluder.getParameters(fig)
			local file = parameters['image']
			local caption = parameters['caption'] or ''
			output = file
		end
	end
	return output
end


return p