Module:Feedback
Appearance
| Main | Documentation | Usage |
This module implements {{Feedback}}.
local p = {}
------------------------------------------------------------
-- Helpers
------------------------------------------------------------
local function isTruthy(value)
if value == nil then
return false
end
value =
mw.text.trim(
tostring(value)
):lower()
return value ~= ''
and value ~= '0'
and value ~= 'false'
and value ~= 'no'
and value ~= 'off'
end
local function trim(value)
return mw.text.trim(
tostring(value or '')
)
end
local function formatUser(user)
user =
trim(user)
if user == ''
or user:lower() == 'anonymous'
then
return 'Anonymous'
end
return string.format(
'[[User:%s|%s]]',
user,
user
)
end
local function currentSubjectPage()
local title =
mw.title.getCurrentTitle()
return title.subjectPageTitle.fullText
end
local function getBucket()
if not (mw.ext and mw.ext.bucket) then
return nil, 'Bucket extension is not available'
end
local bucket =
mw.ext.bucket('feedback')
if not bucket then
return nil, 'Feedback bucket could not be opened'
end
return bucket
end
local function sortNewestFirst(results)
if not results then
return {}
end
table.sort(
results,
function(a, b)
local aDate =
trim(a.createddate)
local bDate =
trim(b.createddate)
if aDate == ''
and bDate ~= ''
then
return false
end
if aDate ~= ''
and bDate == ''
then
return true
end
if aDate ~= bDate then
return aDate > bDate
end
return trim(a.id) > trim(b.id)
end
)
return results
end
local function queryFeedback(
page,
status
)
local bucket, err =
getBucket()
if not bucket then
return nil, err
end
local query =
bucket.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
if page and page ~= '' then
query.where(
'page',
page
)
end
if status
and status ~= ''
and status ~= 'all'
then
query.where(
'status',
status
)
end
local success, results =
pcall(
function()
return query.run()
end
)
if not success then
return nil, tostring(results)
end
return sortNewestFirst(
results or {}
)
end
------------------------------------------------------------
-- Save feedback
------------------------------------------------------------
local function saveFeedback(data)
local bucket, err =
getBucket()
if not bucket then
return false, err
end
local success, saveError =
pcall(
function()
bucket.put({
page =
data.page,
user =
data.user,
status =
data.status,
feedback =
data.feedback,
id =
data.id,
createddate =
data.createddate,
link =
data.link,
image =
data.image,
resolvedby =
data.resolvedby,
resolveddate =
data.resolveddate
})
end
)
if not success then
return false, tostring(saveError)
end
return true
end
------------------------------------------------------------
-- Shared metadata
------------------------------------------------------------
local function renderMetadata(
root,
entry
)
local meta =
root:tag('div')
:addClass(
'feedback-meta'
)
if entry.user
and entry.user ~= ''
then
meta:tag('span')
:addClass(
'feedback-user'
)
:wikitext(
formatUser(
entry.user
)
)
end
if entry.createddate
and entry.createddate ~= ''
then
meta:tag('span')
:addClass(
'feedback-meta-date'
)
:wikitext(
' · ' ..
mw.text.nowiki(
entry.createddate
)
)
end
end
------------------------------------------------------------
-- Attachments
------------------------------------------------------------
local function renderAttachments(
root,
entry
)
local link =
trim(entry.link)
local image =
trim(entry.image)
if link == ''
and image == ''
then
return
end
local attachments =
root:tag('div')
:addClass(
'feedback-attachments'
)
if link ~= '' then
attachments:tag('span')
:addClass(
'feedback-attachment'
)
:wikitext(
'🔗 ' ..
'[' ..
link ..
' Link]'
)
end
if image ~= '' then
local imageAttachment =
attachments:tag('span')
:addClass(
'feedback-attachment'
)
imageAttachment:tag('span')
:addClass(
'feedback-image-icon'
)
:wikitext('▣')
imageAttachment:tag('span')
:addClass(
'feedback-image-link'
)
:wikitext(
'[[Media:' ..
image ..
'|Picture]]'
)
end
end
------------------------------------------------------------
-- Resolution information
------------------------------------------------------------
local function renderResolution(
root,
entry
)
local resolvedBy =
trim(entry.resolvedby)
local resolvedDate =
trim(entry.resolveddate)
if resolvedBy == ''
and resolvedDate == ''
then
return
end
local resolution =
root:tag('div')
:addClass(
'feedback-resolution'
)
resolution:wikitext(
'↳ Resolved by '
)
if resolvedBy ~= '' then
resolution:wikitext(
formatUser(
resolvedBy
)
)
else
resolution:wikitext(
'Unknown'
)
end
if resolvedDate ~= '' then
resolution:wikitext(
' on ' ..
mw.text.nowiki(
resolvedDate
)
)
end
end
------------------------------------------------------------
-- Resolve control
------------------------------------------------------------
local function renderResolveControl(
parent,
id,
page,
compact
)
if not id
or id == ''
then
return
end
local control =
parent:tag('span')
:addClass(
'feedback-resolve-button'
)
control:attr(
'data-feedback-id',
id
)
if page
and page ~= ''
then
control:attr(
'data-feedback-page',
page
)
end
if compact then
control
:addClass(
'feedback-resolve-button--compact'
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'aria-label',
'Resolve feedback'
)
:attr(
'title',
'Resolve feedback'
)
:wikitext(
'✓'
)
else
control
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'aria-label',
'Resolve feedback'
)
:wikitext(
'Resolve'
)
end
end
------------------------------------------------------------
-- Full feedback card
------------------------------------------------------------
local function renderFullCard(
entry
)
local resolved =
entry.status == 'resolved'
local id =
trim(entry.id)
local page =
trim(entry.page)
local root =
mw.html.create('div')
:addClass(
'feedback-box'
)
:addClass(
resolved
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)
if id ~= '' then
root:attr(
'data-feedback-id',
id
)
end
--------------------------------------------------------
-- Main line
--------------------------------------------------------
local main =
root:tag('div')
:addClass(
'feedback-main-line'
)
main:tag('span')
:addClass(
'feedback-status'
)
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'RESOLVED'
or 'UNRESOLVED'
)
main:tag('span')
:addClass(
'feedback-text'
)
:wikitext(
entry.feedback
or 'No feedback provided'
)
--------------------------------------------------------
-- Inline metadata
--------------------------------------------------------
local metadata =
main:tag('span')
:addClass(
'feedback-inline-meta'
)
if entry.user
and entry.user ~= ''
then
metadata:wikitext(
' · ' ..
formatUser(
entry.user
)
)
end
if entry.createddate
and entry.createddate ~= ''
then
metadata:wikitext(
' ' ..
mw.text.nowiki(
entry.createddate
)
)
end
--------------------------------------------------------
-- Resolve button
--------------------------------------------------------
if not resolved
and id ~= ''
then
renderResolveControl(
main,
id,
page,
false
)
end
--------------------------------------------------------
-- Attachments
--------------------------------------------------------
renderAttachments(
root,
entry
)
--------------------------------------------------------
-- Resolution
--------------------------------------------------------
if resolved then
renderResolution(
root,
entry
)
end
--------------------------------------------------------
-- Missing ID
--------------------------------------------------------
if not resolved
and id == ''
then
root:tag('div')
:addClass(
'feedback-missing-id'
)
:wikitext(
'Add an ID to enable the Resolve button.'
)
end
return tostring(root)
end
------------------------------------------------------------
-- Compact article card
------------------------------------------------------------
local function renderCompactCard(
entry
)
local resolved =
entry.status == 'resolved'
local id =
trim(entry.id)
local page =
trim(entry.page)
local root =
mw.html.create('div')
:addClass(
'feedback-page-card'
)
:addClass(
resolved
and 'feedback-page-card--resolved'
or 'feedback-page-card--unresolved'
)
if id ~= '' then
root:attr(
'data-feedback-id',
id
)
end
local content =
root:tag('div')
:addClass(
'feedback-page-card-content'
)
local firstLine =
content:tag('div')
:addClass(
'feedback-page-card-line'
)
firstLine:tag('span')
:addClass(
'feedback-status'
)
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'RESOLVED'
or 'UNRESOLVED'
)
firstLine:tag('span')
:addClass(
'feedback-page-card-text'
)
:wikitext(
entry.feedback
or 'No feedback provided'
)
--------------------------------------------------------
-- Metadata
--------------------------------------------------------
local meta =
content:tag('div')
:addClass(
'feedback-page-card-meta'
)
if entry.user
and entry.user ~= ''
then
meta:wikitext(
formatUser(
entry.user
)
)
end
if entry.createddate
and entry.createddate ~= ''
then
meta:wikitext(
' · ' ..
mw.text.nowiki(
entry.createddate
)
)
end
--------------------------------------------------------
-- Resolution
--------------------------------------------------------
if resolved then
local resolvedBy =
trim(entry.resolvedby)
local resolvedDate =
trim(entry.resolveddate)
if resolvedBy ~= ''
or resolvedDate ~= ''
then
local resolution =
content:tag('div')
:addClass(
'feedback-page-card-resolution'
)
resolution:wikitext(
'↳ Resolved by '
)
if resolvedBy ~= '' then
resolution:wikitext(
formatUser(
resolvedBy
)
)
else
resolution:wikitext(
'Unknown'
)
end
if resolvedDate ~= '' then
resolution:wikitext(
' · ' ..
mw.text.nowiki(
resolvedDate
)
)
end
end
else
if id ~= '' then
renderResolveControl(
root,
id,
page,
true
)
end
end
--------------------------------------------------------
-- Attachments
--------------------------------------------------------
renderAttachments(
content,
entry
)
return tostring(root)
end
------------------------------------------------------------
-- Page feedback section
------------------------------------------------------------
function p.page(frame)
local page =
currentSubjectPage()
local allEntries =
queryFeedback(
page
)
if not allEntries then
return ''
end
if #allEntries == 0 then
return ''
end
local unresolved = {}
local resolved = {}
for _, row in ipairs(allEntries) do
if row.status == 'resolved' then
table.insert(
resolved,
row
)
else
table.insert(
unresolved,
row
)
end
end
local root =
mw.html.create('section')
:addClass(
'feedback-page-section'
)
:attr(
'id',
'feedback-section'
)
:attr(
'data-unresolved-count',
#unresolved
)
:attr(
'data-resolved-count',
#resolved
)
--------------------------------------------------------
-- Heading
--------------------------------------------------------
root:tag('h2')
:addClass(
'feedback-page-heading'
)
:wikitext(
'Feedbacks'
)
--------------------------------------------------------
-- Filters
--------------------------------------------------------
local filters =
root:tag('div')
:addClass(
'feedback-page-filters'
)
local unresolvedFilter =
filters:tag('span')
:addClass(
'feedback-page-filter'
)
:addClass(
'feedback-page-filter--active'
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'data-feedback-filter',
'unresolved'
)
unresolvedFilter:wikitext(
'Unresolved '
)
unresolvedFilter:tag('span')
:addClass(
'feedback-page-filter-count'
)
:wikitext(
tostring(
#unresolved
)
)
if #resolved > 0 then
local resolvedFilter =
filters:tag('span')
:addClass(
'feedback-page-filter'
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'data-feedback-filter',
'resolved'
)
resolvedFilter:wikitext(
'Resolved '
)
resolvedFilter:tag('span')
:addClass(
'feedback-page-filter-count'
)
:wikitext(
tostring(
#resolved
)
)
end
--------------------------------------------------------
-- Unresolved list
--------------------------------------------------------
local unresolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--unresolved'
)
if #unresolved > 0 then
for _, row in ipairs(unresolved) do
unresolvedList:node(
renderCompactCard(
row
)
)
end
else
unresolvedList:tag('div')
:addClass(
'feedback-page-empty'
)
:wikitext(
'No unresolved feedback.'
)
end
--------------------------------------------------------
-- Resolved list
--------------------------------------------------------
if #resolved > 0 then
local resolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--resolved'
)
for _, row in ipairs(resolved) do
resolvedList:node(
renderCompactCard(
row
)
)
end
end
return tostring(root)
end
------------------------------------------------------------
-- Feedback counts
------------------------------------------------------------
function p.counts(frame)
local page =
currentSubjectPage()
local entries =
queryFeedback(
page
)
if not entries then
return ''
end
local unresolved = 0
local resolved = 0
for _, row in ipairs(entries) do
if row.status == 'resolved' then
resolved = resolved + 1
else
unresolved = unresolved + 1
end
end
return tostring(
mw.html.create('span')
:addClass(
'feedback-count-data'
)
:attr(
'data-unresolved',
unresolved
)
:attr(
'data-resolved',
resolved
)
)
end
------------------------------------------------------------
-- Main / individual feedback
------------------------------------------------------------
function p.main(frame)
local parent =
frame:getParent()
local args =
parent
and parent.args
or frame.args
local feedback =
args.feedback
or 'No feedback provided'
local user =
args.user
or 'Anonymous'
local id =
trim(
args.id
)
local createdDate =
trim(
args.createddate
)
local link =
trim(
args.link
)
local image =
trim(
args.image
)
local resolvedBy =
trim(
args.resolvedby
)
local resolvedDate =
trim(
args.resolveddate
)
local resolved =
isTruthy(
args.resolved
)
local status =
resolved
and 'resolved'
or 'unresolved'
local page =
currentSubjectPage()
local saved, saveError =
saveFeedback({
page =
page,
user =
user,
status =
status,
feedback =
feedback,
id =
id,
createddate =
createdDate,
link =
link,
image =
image,
resolvedby =
resolvedBy,
resolveddate =
resolvedDate
})
if not saved then
return
'<strong style="color:red;">' ..
'Feedback bucket error: ' ..
mw.text.nowiki(
saveError or 'Unknown error'
) ..
'</strong>'
end
local card =
renderFullCard({
page =
page,
user =
user,
status =
status,
feedback =
feedback,
id =
id,
createddate =
createdDate,
link =
link,
image =
image,
resolvedby =
resolvedBy,
resolveddate =
resolvedDate
})
local categories = {}
if resolved then
table.insert(
categories,
'[[Category:Resolved Feedback]]'
)
else
table.insert(
categories,
'[[Category:Unresolved Feedback]]'
)
end
if id == '' then
table.insert(
categories,
'[[Category:Feedbacks Missing ID]]'
)
end
return card ..
table.concat(
categories
)
end
------------------------------------------------------------
-- Feedback table
------------------------------------------------------------
local function renderTable(
entries,
includeResolved
)
local tableRoot =
mw.html.create('table')
:addClass(
'feedback-table'
)
local header =
tableRoot:tag('tr')
if includeResolved then
header:tag('th')
:wikitext(
'Status'
)
end
header:tag('th')
:wikitext(
'Page'
)
header:tag('th')
:wikitext(
'Username'
)
header:tag('th')
:wikitext(
'Date'
)
header:tag('th')
:wikitext(
'Feedback'
)
if includeResolved then
header:tag('th')
:wikitext(
'Resolved by'
)
header:tag('th')
:wikitext(
'Resolved date'
)
end
if #entries == 0 then
local row =
tableRoot:tag('tr')
local colspan =
includeResolved
and '7'
or '4'
row:tag('td')
:attr(
'colspan',
colspan
)
:addClass(
'feedback-table-empty'
)
:wikitext(
'No feedback entries found.'
)
return tostring(
tableRoot
)
end
for _, entry in ipairs(entries) do
local row =
tableRoot:tag('tr')
:addClass(
entry.status == 'resolved'
and 'feedback-table-row--resolved'
or 'feedback-table-row--unresolved'
)
if includeResolved then
row:tag('td')
:addClass(
'feedback-table-status'
)
:wikitext(
entry.status == 'resolved'
and 'Resolved'
or 'Unresolved'
)
end
row:tag('td')
:addClass(
'feedback-table-page'
)
:wikitext(
'[[' ..
entry.page ..
']]'
)
row:tag('td')
:addClass(
'feedback-table-user'
)
:wikitext(
formatUser(
entry.user
)
)
row:tag('td')
:addClass(
'feedback-table-date'
)
:wikitext(
mw.text.nowiki(
entry.createddate
or '—'
)
)
row:tag('td')
:addClass(
'feedback-table-feedback'
)
:wikitext(
entry.feedback
or ''
)
if includeResolved then
row:tag('td')
:addClass(
'feedback-table-resolved-by'
)
:wikitext(
entry.resolvedby
and formatUser(
entry.resolvedby
)
or '—'
)
row:tag('td')
:addClass(
'feedback-table-resolved-date'
)
:wikitext(
entry.resolveddate
and mw.text.nowiki(
entry.resolveddate
)
or '—'
)
end
end
return tostring(
tableRoot
)
end
------------------------------------------------------------
-- Global feedback page
------------------------------------------------------------
function p.view(frame)
local parent =
frame:getParent()
local args =
parent
and parent.args
or frame.args
local status =
trim(
args.status or ''
):lower()
--------------------------------------------------------
-- Specific table
--------------------------------------------------------
if status == 'resolved'
or status == 'unresolved'
then
local entries =
queryFeedback(
nil,
status
)
if not entries then
return ''
end
return renderTable(
entries,
false
)
end
--------------------------------------------------------
-- Dashboard
--------------------------------------------------------
local unresolved =
queryFeedback(
nil,
'unresolved'
)
local all =
queryFeedback(
nil,
'all'
)
if not unresolved then
unresolved = {}
end
if not all then
all = {}
end
local root =
mw.html.create('div')
:addClass(
'feedback-dashboard'
)
root:tag('h2')
:addClass(
'feedback-dashboard-heading'
)
:wikitext(
'Unresolved Feedback'
)
root:node(
renderTable(
unresolved,
false
)
)
root:tag('h2')
:addClass(
'feedback-dashboard-heading'
)
:wikitext(
'All Feedback'
)
root:node(
renderTable(
all,
true
)
)
return tostring(
root
)
end
------------------------------------------------------------
-- Aliases
------------------------------------------------------------
p.table = p.view
return p