Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
Created page with "local p = {} ------------------------------------------------------------ -- Helpers ------------------------------------------------------------ local function isTruthy(v) if v == nil then return false end v = mw.text.trim(tostring(v)):lower() return v ~= '' and v ~= '0' and v ~= 'false' and v ~= 'no' and v ~= 'off' end local function formatUser(user) user = mw.text.trim(tostring(user or '')) if user..."
 
ReisuDesign (talk | contribs)
No edit summary
Line 6: Line 6:


local function isTruthy(v)
local function isTruthy(v)
    if v == nil then
if v == nil then
        return false
return false
    end
end


    v = mw.text.trim(tostring(v)):lower()
v = mw.text.trim(tostring(v)):lower()


    return v ~= ''
return v ~= ''
        and v ~= '0'
and v ~= '0'
        and v ~= 'false'
and v ~= 'false'
        and v ~= 'no'
and v ~= 'no'
        and v ~= 'off'
and v ~= 'off'
end
end




local function formatUser(user)
local function formatUser(user)
    user = mw.text.trim(tostring(user or ''))
user = mw.text.trim(tostring(user or ''))


    if user == '' or user:lower() == 'anonymous' then
if user == '' or user:lower() == 'anonymous' then
        return 'Anonymous'
return 'Anonymous'
    end
end


    return string.format(
return string.format(
        '[[User:%s|%s]]',
'[[User:%s|%s]]',
        user,
user,
        user
user
    )
)
end
end


Line 41: Line 41:
local function renderCard(entry, showInstruction)
local function renderCard(entry, showInstruction)


    local resolved = (entry.status == 'resolved')
local resolved = (entry.status == 'resolved')


    local user = entry.user or 'Anonymous'
local user = entry.user or 'Anonymous'
    local feedback = entry.feedback or 'No feedback provided'
local feedback = entry.feedback or 'No feedback provided'
    local page = entry.page
local page = entry.page


    local root = mw.html.create('div')
local root = mw.html.create('div')
        :addClass('feedback-box')
:addClass('feedback-box')
        :addClass(
:addClass(
            resolved
resolved
                and 'feedback-box--resolved'
and 'feedback-box--resolved'
                or 'feedback-box--unresolved'
or 'feedback-box--unresolved'
        )
)




    --------------------------------------------------------
--------------------------------------------------------
    -- Metadata
-- Metadata
    --------------------------------------------------------
--------------------------------------------------------


    local meta = root:tag('div')
local meta = root:tag('div')
        :addClass('feedback-meta')
:addClass('feedback-meta')




    if page and page ~= '' then
if page and page ~= '' then


        meta:tag('span')
meta:tag('span')
            :addClass('feedback-page')
:addClass('feedback-page')
            :wikitext(
:wikitext(
                string.format('[[%s]]', page)
string.format('[[%s]]', page)
            )
)


        meta:tag('span')
meta:tag('span')
            :addClass('feedback-separator')
:addClass('feedback-separator')
            :wikitext(' • ')
:wikitext(' • ')


    end
end




    meta:tag('span')
meta:tag('span')
        :addClass('feedback-user')
:addClass('feedback-user')
        :wikitext(formatUser(user))
:wikitext(formatUser(user))




    meta:tag('span')
meta:tag('span')
        :addClass('feedback-separator')
:addClass('feedback-separator')
        :wikitext(' • ')
:wikitext(' • ')




    meta:tag('span')
meta:tag('span')
        :addClass('feedback-status')
:addClass('feedback-status')
        :addClass(
:addClass(
            resolved
resolved
                and 'feedback-status--resolved'
and 'feedback-status--resolved'
                or 'feedback-status--unresolved'
or 'feedback-status--unresolved'
        )
)
        :wikitext(
:wikitext(
            resolved
resolved
                and 'Resolved'
and 'Resolved'
                or 'Unresolved'
or 'Unresolved'
        )
)




    --------------------------------------------------------
--------------------------------------------------------
    -- Feedback text
-- Feedback text
    --------------------------------------------------------
--------------------------------------------------------


    root:tag('div')
root:tag('div')
        :addClass('feedback-body')
:addClass('feedback-body')
        :wikitext(feedback)
:wikitext(feedback)




    --------------------------------------------------------
--------------------------------------------------------
    -- Resolution instruction
-- Resolution instruction
    --------------------------------------------------------
--------------------------------------------------------


    if showInstruction and not resolved then
if showInstruction and not resolved then


        root:tag('div')
root:tag('div')
            :addClass('feedback-instruction')
:addClass('feedback-instruction')
            :wikitext(
:wikitext(
                'If this feedback has been resolved, edit this page '
'If this feedback has been resolved, edit this page '
                .. 'and set <code>|resolved=true</code>.'
.. 'and set <code>|resolved=true</code>.'
            )
)


    end
end




    return tostring(root)
return tostring(root)
end
end


Line 138: Line 138:
local function saveFeedback(data)
local function saveFeedback(data)


    if not (mw.ext and mw.ext.bucket) then
if not (mw.ext and mw.ext.bucket) then
        return false
return false, 'Bucket extension is not available'
    end
end


    local bucket = mw.ext.bucket('feedback')


    if not bucket then
local bucket = mw.ext.bucket('feedback')
        return false
    end


if not bucket then
return false, 'Feedback bucket could not be opened'
end


    local success = pcall(function()


        bucket.put({
local success, err = pcall(function()
            page = data.page,
            user = data.user,
            status = data.status,
            feedback = data.feedback
        })


    end)
bucket.put({
page = data.page,
user = data.user,
status = data.status,
feedback = data.feedback
})


end)


    return success
 
if not success then
return false, tostring(err)
end
 
 
return true
end
end


Line 181: Line 187:
function p.main(frame)
function p.main(frame)


    local parent = frame:getParent()
local parent = frame:getParent()


    local args =
local args =
        parent and parent.args
parent and parent.args
        or frame.args
or frame.args




    --------------------------------------------------------
--------------------------------------------------------
    -- Get page title
-- Get page title
    --------------------------------------------------------
--------------------------------------------------------


    local pageTitle = nil
local pageTitle = nil


    if parent and parent.getTitle then
if parent and parent.getTitle then


        local title = parent:getTitle()
local title = parent:getTitle()


        if title
if title
            and title ~= ''
and title ~= ''
            and not title:find('^Template:')
and not title:find('^Template:')
        then
then
            pageTitle = title
pageTitle = title
        end
end


    end
end




    return p._main(args, pageTitle)
return p._main(args, pageTitle)


end
end
Line 215: Line 221:
function p._main(args, pageTitle)
function p._main(args, pageTitle)


    local feedback =
local feedback =
        args.feedback
args.feedback
        or 'No feedback provided'
or 'No feedback provided'
 
 
local user =
args.user
or 'Anonymous'
 
 
local resolved =
isTruthy(args.resolved)




    local user =
local status =
        args.user
resolved
        or 'Anonymous'
and 'resolved'
or 'unresolved'




    local resolved =
local currentPage =
        isTruthy(args.resolved)
pageTitle
or mw.title.getCurrentTitle().fullText




    local status =
--------------------------------------------------------
        resolved
-- Save to Bucket
            and 'resolved'
--------------------------------------------------------
            or 'unresolved'


local saved, saveError = saveFeedback({
page = currentPage,
user = user,
status = status,
feedback = feedback
})


    local currentPage =
        pageTitle
        or mw.title.getCurrentTitle().fullText


if not saved then


    --------------------------------------------------------
return '<strong style="color:red;">Feedback bucket error: '
    -- Save to Bucket
.. mw.text.nowiki(saveError or 'Unknown error')
    --------------------------------------------------------
.. '</strong>'


    saveFeedback({
end
        page = currentPage,
        user = user,
        status = status,
        feedback = feedback
    })




    --------------------------------------------------------
--------------------------------------------------------
    -- Render the feedback card
-- Render feedback card
    --------------------------------------------------------
--------------------------------------------------------


    local card = renderCard({
local card = renderCard({


        page = nil,
page = nil,


        user = user,
user = user,


        status = status,
status = status,


        feedback = feedback
feedback = feedback


    }, true)
}, true)




    --------------------------------------------------------
--------------------------------------------------------
    -- Categorize feedback
-- Categorize feedback
    --------------------------------------------------------
--------------------------------------------------------


    local category
local category


    if resolved then
if resolved then


        category =
category =
            '[[Category:Resolved Feedback]]'
'[[Category:Resolved Feedback]]'


    else
else


        category =
category =
            '[[Category:Unresolved Feedback]]'
'[[Category:Unresolved Feedback]]'


    end
end




    return card .. category
return card .. category


end
end
Line 299: Line 314:
local function renderFeedbackList(results)
local function renderFeedbackList(results)


    if not results or #results == 0 then
if not results or #results == 0 then
        return '<em>No feedback entries found.</em>'
return '<em>No feedback entries found.</em>'
    end
end




    local list = {}
local list = {}




    for _, row in ipairs(results) do
for _, row in ipairs(results) do


        table.insert(
table.insert(
            list,
list,
            renderCard(row, false)
renderCard(row, false)
        )
)


    end
end




    return table.concat(list, '\n')
return table.concat(list, '\n')


end
end
Line 338: Line 353:
function p.view(frame)
function p.view(frame)


    local parent = frame:getParent()
local parent = frame:getParent()




    local args
local args


    if frame
if frame
        and frame.args
and frame.args
        and frame.args.status
and frame.args.status
    then
then


        args = frame.args
args = frame.args


    elseif parent
elseif parent
        and parent.args
and parent.args
        and parent.args.status
and parent.args.status
    then
then


        args = parent.args
args = parent.args


    elseif frame
elseif frame
        and frame.args
and frame.args
    then
then


        args = frame.args
args = frame.args


    elseif parent
elseif parent
        and parent.args
and parent.args
    then
then


        args = parent.args
args = parent.args


    else
else


        args = {}
args = {}


    end
end




    return p._view(args, frame)
return p._view(args, frame)


end
end
Line 383: Line 398:
function p._view(args, frame)
function p._view(args, frame)


    --------------------------------------------------------
--------------------------------------------------------
    -- Check Bucket
-- Check Bucket
    --------------------------------------------------------
--------------------------------------------------------


    if not (mw.ext and mw.ext.bucket) then
if not (mw.ext and mw.ext.bucket) then


        return
return
            '<em>Bucket extension is not available.</em>'
'<em>Bucket extension is not available.</em>'


    end
end




    frame = frame or mw.getCurrentFrame()
frame = frame or mw.getCurrentFrame()




    --------------------------------------------------------
--------------------------------------------------------
    -- Determine requested status
-- Determine requested status
    --------------------------------------------------------
--------------------------------------------------------


    local rawStatus =
local rawStatus =
        args.status
args.status
        or args[1]
or args[1]




    local filterStatus =
local filterStatus =
        rawStatus
rawStatus
        and mw.text.trim(rawStatus):lower()
and mw.text.trim(rawStatus):lower()
        or nil
or nil




    --------------------------------------------------------
--------------------------------------------------------
    -- Specific status
-- Specific status
    --------------------------------------------------------
--------------------------------------------------------


    if filterStatus == 'resolved'
if filterStatus == 'resolved'
        or filterStatus == 'unresolved'
or filterStatus == 'unresolved'
    then
then


        local query =
local query =
            mw.ext.bucket('feedback')
mw.ext.bucket('feedback')




        query.select(
query.select(
            'page',
'page',
            'user',
'user',
            'status',
'status',
            'feedback'
'feedback'
        )
)




        query.where(
query.where(
            'status',
'status',
            filterStatus
filterStatus
        )
)




        return renderFeedbackList(
return renderFeedbackList(
            query.run()
query.run()
        )
)


    end
end




    --------------------------------------------------------
--------------------------------------------------------
    -- All feedback
-- All feedback
    --------------------------------------------------------
--------------------------------------------------------


    if filterStatus == 'all' then
if filterStatus == 'all' then


        local query =
local query =
            mw.ext.bucket('feedback')
mw.ext.bucket('feedback')




        query.select(
query.select(
            'page',
'page',
            'user',
'user',
            'status',
'status',
            'feedback'
'feedback'
        )
)




        return renderFeedbackList(
return renderFeedbackList(
            query.run()
query.run()
        )
)


    end
end




    --------------------------------------------------------
--------------------------------------------------------
    -- Tabber mode
-- Tabber mode
    --
--
    -- Default:
-- Default:
    --
--
    -- All
-- All
    -- Unresolved
-- Unresolved
    -- Resolved
-- Resolved
    --------------------------------------------------------
--------------------------------------------------------


    local qAll =
local qAll =
        mw.ext.bucket('feedback')
mw.ext.bucket('feedback')




    qAll.select(
qAll.select(
        'page',
'page',
        'user',
'user',
        'status',
'status',
        'feedback'
'feedback'
    )
)




    local allEntries =
local allEntries =
        qAll.run()
qAll.run()




    local allList = {}
local allList = {}
    local unresolvedList = {}
local unresolvedList = {}
    local resolvedList = {}
local resolvedList = {}




    --------------------------------------------------------
--------------------------------------------------------
    -- Split entries by status
-- Split entries by status
    --------------------------------------------------------
--------------------------------------------------------


    if allEntries and #allEntries > 0 then
if allEntries and #allEntries > 0 then


        for _, row in ipairs(allEntries) do
for _, row in ipairs(allEntries) do


            local card =
local card =
                renderCard(row, false)
renderCard(row, false)




            table.insert(
table.insert(
                allList,
allList,
                card
card
            )
)




            if row.status == 'resolved' then
if row.status == 'resolved' then


                table.insert(
table.insert(
                    resolvedList,
resolvedList,
                    card
card
                )
)


            else
else


                table.insert(
table.insert(
                    unresolvedList,
unresolvedList,
                    card
card
                )
)


            end
end


        end
end


    end
end




    --------------------------------------------------------
--------------------------------------------------------
    -- Build tab contents
-- Build tab contents
    --------------------------------------------------------
--------------------------------------------------------


    local allContent
local allContent


    if #allList > 0 then
if #allList > 0 then


        allContent =
allContent =
            table.concat(allList, '\n')
table.concat(allList, '\n')


    else
else


        allContent =
allContent =
            '<em>No feedback entries found.</em>'
'<em>No feedback entries found.</em>'


    end
end




    local unresolvedContent
local unresolvedContent


    if #unresolvedList > 0 then
if #unresolvedList > 0 then


        unresolvedContent =
unresolvedContent =
            table.concat(unresolvedList, '\n')
table.concat(unresolvedList, '\n')


    else
else


        unresolvedContent =
unresolvedContent =
            '<em>No unresolved feedback entries found.</em>'
'<em>No unresolved feedback entries found.</em>'


    end
end




    local resolvedContent
local resolvedContent


    if #resolvedList > 0 then
if #resolvedList > 0 then


        resolvedContent =
resolvedContent =
            table.concat(resolvedList, '\n')
table.concat(resolvedList, '\n')


    else
else


        resolvedContent =
resolvedContent =
            '<em>No resolved feedback entries found.</em>'
'<em>No resolved feedback entries found.</em>'


    end
end




    --------------------------------------------------------
--------------------------------------------------------
    -- Tabber
-- Tabber
    --------------------------------------------------------
--------------------------------------------------------


    local tabberBody = string.format(
local tabberBody = string.format(
        'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
        allContent,
allContent,
        unresolvedContent,
unresolvedContent,
        resolvedContent
resolvedContent
    )
)




    return frame:callParserFunction(
return frame:callParserFunction(
        '#tag',
'#tag',
        {
{
            'tabber',
'tabber',
            tabberBody
tabberBody
        }
}
    )
)


end
end

Revision as of 04:53, 16 September 2026

Module documentation

This module implements {{Feedback}}.

local p = {}

------------------------------------------------------------
-- Helpers
------------------------------------------------------------

local function isTruthy(v)
	if v == nil then
		return false
	end

	v = mw.text.trim(tostring(v)):lower()

	return v ~= ''
		and v ~= '0'
		and v ~= 'false'
		and v ~= 'no'
		and v ~= 'off'
end


local function formatUser(user)
	user = mw.text.trim(tostring(user or ''))

	if user == '' or user:lower() == 'anonymous' then
		return 'Anonymous'
	end

	return string.format(
		'[[User:%s|%s]]',
		user,
		user
	)
end


------------------------------------------------------------
-- Feedback card
------------------------------------------------------------

local function renderCard(entry, showInstruction)

	local resolved = (entry.status == 'resolved')

	local user = entry.user or 'Anonymous'
	local feedback = entry.feedback or 'No feedback provided'
	local page = entry.page

	local root = mw.html.create('div')
		:addClass('feedback-box')
		:addClass(
			resolved
				and 'feedback-box--resolved'
				or 'feedback-box--unresolved'
		)


	--------------------------------------------------------
	-- Metadata
	--------------------------------------------------------

	local meta = root:tag('div')
		:addClass('feedback-meta')


	if page and page ~= '' then

		meta:tag('span')
			:addClass('feedback-page')
			:wikitext(
				string.format('[[%s]]', page)
			)

		meta:tag('span')
			:addClass('feedback-separator')
			:wikitext(' • ')

	end


	meta:tag('span')
		:addClass('feedback-user')
		:wikitext(formatUser(user))


	meta:tag('span')
		:addClass('feedback-separator')
		:wikitext(' • ')


	meta:tag('span')
		:addClass('feedback-status')
		:addClass(
			resolved
				and 'feedback-status--resolved'
				or 'feedback-status--unresolved'
		)
		:wikitext(
			resolved
				and 'Resolved'
				or 'Unresolved'
		)


	--------------------------------------------------------
	-- Feedback text
	--------------------------------------------------------

	root:tag('div')
		:addClass('feedback-body')
		:wikitext(feedback)


	--------------------------------------------------------
	-- Resolution instruction
	--------------------------------------------------------

	if showInstruction and not resolved then

		root:tag('div')
			:addClass('feedback-instruction')
			:wikitext(
				'If this feedback has been resolved, edit this page '
				.. 'and set <code>|resolved=true</code>.'
			)

	end


	return tostring(root)
end


------------------------------------------------------------
-- Save feedback
------------------------------------------------------------

local function saveFeedback(data)

	if not (mw.ext and mw.ext.bucket) then
		return false, 'Bucket extension is not available'
	end


	local bucket = mw.ext.bucket('feedback')

	if not bucket then
		return false, 'Feedback bucket could not be opened'
	end


	local success, err = pcall(function()

		bucket.put({
			page = data.page,
			user = data.user,
			status = data.status,
			feedback = data.feedback
		})

	end)


	if not success then
		return false, tostring(err)
	end


	return true
end


------------------------------------------------------------
-- Main
--
-- Used by Template:Feedback
--
-- Example:
--
-- {{Feedback
--  |feedback=Something could be improved here.
--  |user=Rei
--  |resolved=false
-- }}
------------------------------------------------------------

function p.main(frame)

	local parent = frame:getParent()

	local args =
		parent and parent.args
		or frame.args


	--------------------------------------------------------
	-- Get page title
	--------------------------------------------------------

	local pageTitle = nil

	if parent and parent.getTitle then

		local title = parent:getTitle()

		if title
			and title ~= ''
			and not title:find('^Template:')
		then
			pageTitle = title
		end

	end


	return p._main(args, pageTitle)

end


function p._main(args, pageTitle)

	local feedback =
		args.feedback
		or 'No feedback provided'


	local user =
		args.user
		or 'Anonymous'


	local resolved =
		isTruthy(args.resolved)


	local status =
		resolved
			and 'resolved'
			or 'unresolved'


	local currentPage =
		pageTitle
		or mw.title.getCurrentTitle().fullText


	--------------------------------------------------------
	-- Save to Bucket
	--------------------------------------------------------

	local saved, saveError = saveFeedback({
		page = currentPage,
		user = user,
		status = status,
		feedback = feedback
	})


	if not saved then

		return '<strong style="color:red;">Feedback bucket error: '
			.. mw.text.nowiki(saveError or 'Unknown error')
			.. '</strong>'

	end


	--------------------------------------------------------
	-- Render feedback card
	--------------------------------------------------------

	local card = renderCard({

		page = nil,

		user = user,

		status = status,

		feedback = feedback

	}, true)


	--------------------------------------------------------
	-- Categorize feedback
	--------------------------------------------------------

	local category

	if resolved then

		category =
			'[[Category:Resolved Feedback]]'

	else

		category =
			'[[Category:Unresolved Feedback]]'

	end


	return card .. category

end


------------------------------------------------------------
-- Render feedback list
------------------------------------------------------------

local function renderFeedbackList(results)

	if not results or #results == 0 then
		return '<em>No feedback entries found.</em>'
	end


	local list = {}


	for _, row in ipairs(results) do

		table.insert(
			list,
			renderCard(row, false)
		)

	end


	return table.concat(list, '\n')

end


------------------------------------------------------------
-- View feedback
--
-- Examples:
--
-- {{#invoke:Feedback|view}}
--
-- {{#invoke:Feedback|view|status=all}}
--
-- {{#invoke:Feedback|view|status=resolved}}
--
-- {{#invoke:Feedback|view|status=unresolved}}
------------------------------------------------------------

function p.view(frame)

	local parent = frame:getParent()


	local args

	if frame
		and frame.args
		and frame.args.status
	then

		args = frame.args

	elseif parent
		and parent.args
		and parent.args.status
	then

		args = parent.args

	elseif frame
		and frame.args
	then

		args = frame.args

	elseif parent
		and parent.args
	then

		args = parent.args

	else

		args = {}

	end


	return p._view(args, frame)

end


function p._view(args, frame)

	--------------------------------------------------------
	-- Check Bucket
	--------------------------------------------------------

	if not (mw.ext and mw.ext.bucket) then

		return
			'<em>Bucket extension is not available.</em>'

	end


	frame = frame or mw.getCurrentFrame()


	--------------------------------------------------------
	-- Determine requested status
	--------------------------------------------------------

	local rawStatus =
		args.status
		or args[1]


	local filterStatus =
		rawStatus
		and mw.text.trim(rawStatus):lower()
		or nil


	--------------------------------------------------------
	-- Specific status
	--------------------------------------------------------

	if filterStatus == 'resolved'
		or filterStatus == 'unresolved'
	then

		local query =
			mw.ext.bucket('feedback')


		query.select(
			'page',
			'user',
			'status',
			'feedback'
		)


		query.where(
			'status',
			filterStatus
		)


		return renderFeedbackList(
			query.run()
		)

	end


	--------------------------------------------------------
	-- All feedback
	--------------------------------------------------------

	if filterStatus == 'all' then

		local query =
			mw.ext.bucket('feedback')


		query.select(
			'page',
			'user',
			'status',
			'feedback'
		)


		return renderFeedbackList(
			query.run()
		)

	end


	--------------------------------------------------------
	-- Tabber mode
	--
	-- Default:
	--
	-- All
	-- Unresolved
	-- Resolved
	--------------------------------------------------------

	local qAll =
		mw.ext.bucket('feedback')


	qAll.select(
		'page',
		'user',
		'status',
		'feedback'
	)


	local allEntries =
		qAll.run()


	local allList = {}
	local unresolvedList = {}
	local resolvedList = {}


	--------------------------------------------------------
	-- Split entries by status
	--------------------------------------------------------

	if allEntries and #allEntries > 0 then

		for _, row in ipairs(allEntries) do

			local card =
				renderCard(row, false)


			table.insert(
				allList,
				card
			)


			if row.status == 'resolved' then

				table.insert(
					resolvedList,
					card
				)

			else

				table.insert(
					unresolvedList,
					card
				)

			end

		end

	end


	--------------------------------------------------------
	-- Build tab contents
	--------------------------------------------------------

	local allContent

	if #allList > 0 then

		allContent =
			table.concat(allList, '\n')

	else

		allContent =
			'<em>No feedback entries found.</em>'

	end


	local unresolvedContent

	if #unresolvedList > 0 then

		unresolvedContent =
			table.concat(unresolvedList, '\n')

	else

		unresolvedContent =
			'<em>No unresolved feedback entries found.</em>'

	end


	local resolvedContent

	if #resolvedList > 0 then

		resolvedContent =
			table.concat(resolvedList, '\n')

	else

		resolvedContent =
			'<em>No resolved feedback entries found.</em>'

	end


	--------------------------------------------------------
	-- Tabber
	--------------------------------------------------------

	local tabberBody = string.format(
		'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
		allContent,
		unresolvedContent,
		resolvedContent
	)


	return frame:callParserFunction(
		'#tag',
		{
			'tabber',
			tabberBody
		}
	)

end


return p