Jump to content

Module:Database report

Permanently protected module
From Wikipedia, the free encyclopedia

--[[

Lua module for generating {{database report}} configurations.

This module provides a class-based interface to create database report configurations.

Usage from Lua:

local p = {}
p.main = function() 
  local Report = require('Module:Database report')
  local report = Report:new()
  report:setSQL("SELECT page_title FROM page LIMIT 10")
  report:useWikilinks(1)
  report:setInterval(7)
  return report:generate()
end
return p
]]

local Report = {}
Report.__index = Report

--- Initialize a new database report instance.
-- This is the constructor method that should be called to create a new report instance.
-- @return Report A new Report instance with default settings
-- @usage local report = Report:new()
function Report:new()
    local obj = {
        sql = nil,
        wikilinks = {},
        widths = {},
        comments = {},
        remove_underscores = {},
        hide = {},
        excerpts = {},
        table_style = nil,
        table_class = nil,
        interval = nil,
        pagination = nil,
        max_pages = nil,
        row_template = nil,
        row_template_named_params = nil,
        skip_table = nil,
        header_template = nil,
        footer_template = nil,
        postprocess_js = nil,
        silent = nil,
        head_content = nil
    }
    setmetatable(obj, self)
    return obj
end

--- Set the SQL query for the database report.
-- This is a required parameter.
-- The SQL must be a valid SELECT statement.
-- @param sql string The SQL query to execute (must be non-empty)
-- @return Report Returns self for method chaining
-- @raise error if sql is nil, not a string, or empty
-- @usage report:setSQL("SELECT page_title FROM page LIMIT 10")
function Report:setSQL(sql)
    if not sql or type(sql) ~= "string" or sql:match("^%s*$") then
        error("SQL must be a non-empty string")
    end
    self.sql = sql
    return self
end

--- Configure wikilinks for one or more columns in the report.
-- Wikilinks convert column values into clickable links to other pages.
-- Can be called multiple times to configure different columns.
-- @param column number|table Column number to be wikilinked, or table of column configurations
-- @param namespace number|string Optional namespace number for links. If the namespace number is in another column, use "c1" if it's in the first column, "c2" if it's in the second column, etc.
-- @param show boolean Whether the namespace prefix should be displayed in the link (default: false)
-- @return Report Returns self for method chaining
-- @usage report:useWikilinks(1) -- Simple wikilink for column 1
-- @usage report:useWikilinks(1, 0, true) -- Column 1, namespace 0, show prefix
-- @usage report:useWikilinks({{column=1, namespace="c2"}, {column=3}}) -- Multiple columns
function Report:useWikilinks(column, namespace, show)
    if type(column) == "number" then
        table.insert(self.wikilinks, {
            column = column,
            namespace = namespace,
            show = show
        })
    elseif type(column) == "table" then
        -- Support for multiple wikilinks at once
        for _, link in ipairs(column) do
            table.insert(self.wikilinks, link)
        end
    end
    return self
end

--- Set the width for one or more columns in the report.
-- Controls the display width of columns using CSS width values.
-- Can be called multiple times to set different column widths.
-- @param column number|table Column number to set width for, or table of width configurations
-- @param width string CSS width value (e.g., "10em", "50px", "20%", "200px")
-- @return Report Returns self for method chaining
-- @usage report:setWidth(1, "200px") -- Set column 1 to 200px width
-- @usage report:setWidth({{column=1, width="10em"}, {column=2, width="50%"}}) -- Multiple columns
function Report:setWidth(column, width)
    if type(column) == "number" and type(width) == "string" then
        table.insert(self.widths, {column = column, width = width})
    elseif type(column) == "table" then
        -- Support for multiple widths at once
        for _, w in ipairs(column) do
            table.insert(self.widths, w)
        end
    end
    return self
end

--- Set columns to be treated as comments (edit summaries or log action summaries) in the report.
-- @param ... number Variable number of column numbers to mark as comments
-- @return Report Returns self for method chaining
-- @usage report:setCommentColumns(3, 5) -- Mark columns 3 and 5 as comments
function Report:setCommentColumns(...)
    for _, col in ipairs({...}) do
        if type(col) == "number" then
            table.insert(self.comments, col)
        end
    end
    return self
end

--- Configure columns to have underscores removed from their values.
-- Useful for page titles where underscores should be converted to spaces. For columns with wikilinks or excerpts configured, this will be done automatically.
-- @param ... number Variable number of column numbers to remove underscores from
-- @return Report Returns self for method chaining
-- @usage report:removeUnderscores(1, 2) -- Remove underscores from columns 1 and 2
function Report:removeUnderscores(...)
    for _, col in ipairs({...}) do
        if type(col) == "number" then
            table.insert(self.remove_underscores, col)
        end
    end
    return self
end

--- Hide specified columns from the report display.
-- Hidden columns are still processed but not shown in the final output.
-- Useful for columns used only for processing (like namespace numbers) but not for display.
-- @param ... number Variable number of column numbers to hide
-- @return Report Returns self for method chaining
-- @usage report:hideColumns(2, 4) -- Hide columns 2 and 4 from display
function Report:hideColumns(...)
    for _, col in ipairs({...}) do
        if type(col) == "number" then
            table.insert(self.hide, col)
        end
    end
    return self
end

--- Configure generation of article excerpts (summaries).
-- Creates excerpts from source column that should contain page titles, and places them in a destination column.
-- @param srcColumn number Column containing page title whose excerpt should be created
-- @param destColumn number Destination column number to place the excerpt. 
-- @param namespace number|string Namespace number for the page title (default: 0 for main namespace). If the namespace number is in another column, use "c1" if it's in the first column, "c2" if it's in the second column, etc.
-- @param charLimit number Optional character limit for the excerpt
-- @param charHardLimit number Optional hard character limit (truncates if exceeded)
-- @return Report Returns self for method chaining
-- @usage report:useExcerpt(1, 2) -- Create excerpt for titles in column 1, place in column 2
-- @usage report:useExcerpt(1, 2, 0, 200, 250) -- Create excerpt for titles in column 1 with namespace 2 (user namespace), 200 char limit, 250 hard limit
function Report:useExcerpt(srcColumn, destColumn, namespace, charLimit, charHardLimit)
    table.insert(self.excerpts, {
        srcColumn = srcColumn,
        destColumn = destColumn,
        namespace = namespace,
        charLimit = charLimit,
        charHardLimit = charHardLimit
    })
    return self
end

--- Set the CSS style for the report table.
-- Applies custom CSS styling to the generated table element.
-- @param style string CSS style string to apply to the table
-- @return Report Returns self for method chaining
-- @usage report:setTableStyle("border: 1px solid #ccc; width: 100%")
function Report:setTableStyle(style)
    self.table_style = style
    return self
end

--- Set the CSS class for the report table.
-- Applies a CSS class to the generated table element for styling.
-- @param class string CSS class name to apply to the table
-- @return Report Returns self for method chaining
-- @usage report:setTableClass("wikitable sortable")
function Report:setTableClass(class)
    self.table_class = class
    return self
end

--- Set the update interval for the database report.
-- Controls how often the report is automatically refreshed with new data from the database.
-- @param days number Number of days between updates (must be >= 1)
-- @return Report Returns self for method chaining
-- @raise error if days is not a number or is less than 1
-- @usage report:setInterval(7) -- Update every 7 days
function Report:setInterval(days)
    if type(days) == "number" and days >= 1 then
        self.interval = days
    else
        error("Interval must be a number >= 1")
    end
    return self
end

--- Set the number of rows per page for pagination.
-- Enables pagination by limiting the number of rows displayed per page. By default, no pagination is done.
-- @param count number Number of rows to display per page. 
-- @return Report Returns self for method chaining
-- @raise error if count is not a positive number
-- @usage report:setPagination(1000) -- Show 1000 rows per page
function Report:setPagination(count)
    if type(count) == "number" and count > 0 then
        self.pagination = count
    else
        error("Pagination must be a positive number")
    end
    return self
end

--- Set the maximum number of pages when using pagination.
-- Limits the total number of pages that can be created in a paginated report. The default value is 5. 
-- @param count number Maximum number of pages to display. Should not be greater than 20.
-- @return Report Returns self for method chaining
-- @raise error if count is not a positive number
-- @usage report:setMaxPages(10) -- Limit to 10 pages maximum
function Report:setMaxPages(count)
    if type(count) == "number" and count > 0 then
        self.max_pages = count
    else
        error("Max pages must be a positive number")
    end
    return self
end

--- Set a template to be used for formatting each row.
-- Allows custom formatting of individual rows using MediaWiki templates. The "Template:" prefix can be skipped.
-- @param template string Name of the template to use for row formatting
-- @return Report Returns self for method chaining
-- @usage report:setRowTemplate("MyRowTemplate")
function Report:setRowTemplate(template)
    self.row_template = template
    return self
end

--- Enable use of named parameters in the row template.
-- When enabled, values to the row template are passed by column name instead of position. For a query with columns page_title, page_namespace and rev_len, the row template generated would be {{MyRowTemplate|page_title=...|page_namespace=...|rev_len=...}} instead of {{MyRowTemplate|1=...|2=...|3=...}}.
-- @param value boolean Whether to use named parameters (truthy/falsy)
-- @return Report Returns self for method chaining
-- @usage report:useNamedParamsInRowTemplate(true) -- Use named parameters
function Report:useNamedParamsInRowTemplate(value)
    self.row_template_named_params = value
    return self
end

--- Skip generating the table structure for the report.
-- Useful when row_template is used and the table structure is not needed. 
-- Useful when using custom templates for display.
-- @param value boolean Whether to skip table generation (truthy/falsy)
-- @return Report Returns self for method chaining
-- @usage report:skipTable(true) -- Skip table generation
function Report:skipTable(value)
    self.skip_table = value
    return self
end

--- Set a template to be used for generating the table header.
-- @param template string Name of the template to use for the header
-- @return Report Returns self for method chaining
-- @usage report:setHeaderTemplate("MyHeaderTemplate")
function Report:setHeaderTemplate(template)
    self.header_template = template
    return self
end

--- Set a template to be used as the report footer.
-- This can be used to complement the header template. For example, if the header template is {{div col}}, the footer template can be {{div col end}}.
-- @param template string Name of the template to use for the footer
-- @return Report Returns self for method chaining
-- @usage report:setFooterTemplate("MyFooterTemplate")
function Report:setFooterTemplate(template)
    self.footer_template = template
    return self
end

--- Set JavaScript code to be executed for postprocessing the report content.
-- Allows for arbitrary manipulation of the report data with limited access to Wikimedia APIs.
-- See [[Template:Database report#postprocess_js]] for more details.
-- @param js string JavaScript code
-- @return Report Returns self for method chaining
-- @usage report:setPostprocessJS("console.log('Report loaded');")
function Report:setPostprocessJS(js)
    self.postprocess_js = js
    return self
end

--- Enable or disable silent mode.
-- In silent mode, all visible output from the template is suppressed. Only the generated table is shown.
-- @param value boolean Whether to enable silent mode (truthy/falsy)
-- @return Report Returns self for method chaining
-- @usage report:setSilent(true) -- Enable silent mode
function Report:setSilent(value)
    self.silent = value
    return self
end

--- Set content placed before the report. Not to be confused with [[#Report:setHeaderTemplate|setHeaderTemplate]].
-- This can be used to include some lead text before the report, templatestyles tags, etc.
-- @param value Text 
-- @return Report Returns self for method chaining
-- @usage report:setHeadContent('This appears before the report')
function Report:setHeadContent(value)
	self.head_content = value
	return self
end


--- Generate the complete database report configuration.
-- This is the main method that produces the output for use by the bot.
-- @return string Complete database report template configuration
-- @raise error if validation fails (e.g., missing SQL)
-- @usage return report:generate() -- Return the serialized representation of the report config
function Report:generate()
    if not self.sql then
        error("SQL must be set before generating")
    end
    
    -- Build the configuration
    local config = {}
    
    -- Add SQL (required)
    table.insert(config, "|sql = " .. self.sql)
    
    -- Add optional parameters
    local optionalParams = {
        wikilinks = formatWikilinks(self.wikilinks),
        comments = formatColumnList(self.comments),
        widths = formatWidths(self.widths),
        table_style = self.table_style,
        table_class = self.table_class,
        excerpts = formatExcerpts(self.excerpts),
        remove_underscores = formatColumnList(self.remove_underscores),
        interval = self.interval,
        pagination = self.pagination,
        max_pages = self.max_pages,
        hide = formatColumnList(self.hide),
        row_template = self.row_template,
        row_template_named_params = self.row_template_named_params,
        skip_table = self.skip_table,
        header_template = self.header_template,
        footer_template = self.footer_template,
        postprocess_js = self.postprocess_js,
        silent = self.silent,
        head_content = self.head_content
    }
    
    -- Add non-empty optional parameters
    for param, value in pairs(optionalParams) do
        if value and value ~= "" then
            table.insert(config, "|" .. param .. " = " .. tostring(value))
        end
    end
    
    -- Join configuration lines
    local configText = table.concat(config, "\n")
    
    -- Return the complete database report template
    return "{{Database report\n" .. configText .. "\n}}"
end

-- Helper function to format wikilinks
function formatWikilinks(wikilinks)
    if not wikilinks or #wikilinks == 0 then return nil end
    
    local parts = {}
    for _, link in ipairs(wikilinks) do
        if type(link) == "table" and link.column then
            local part = tostring(link.column)
            if link.namespace then
                part = part .. ":" .. tostring(link.namespace)
            end
            if link.show then
                part = part .. ":show"
            end
            table.insert(parts, part)
        elseif type(link) == "string" then
            table.insert(parts, link)
        end
    end
    
    return #parts > 0 and table.concat(parts, ", ") or nil
end

-- Helper function to format widths
function formatWidths(widths)
    if not widths or #widths == 0 then return nil end
    
    local parts = {}
    for _, width in ipairs(widths) do
        if type(width) == "table" and width.column and width.width then
            table.insert(parts, tostring(width.column) .. ":" .. width.width)
        elseif type(width) == "string" then
            table.insert(parts, width)
        end
    end
    
    return #parts > 0 and table.concat(parts, ", ") or nil
end

-- Helper function to format column lists
function formatColumnList(columns)
    if not columns or #columns == 0 then return nil end
    
    local parts = {}
    for _, col in ipairs(columns) do
        if type(col) == "number" then
            table.insert(parts, tostring(col))
        elseif type(col) == "string" and col:match("^%d+$") then
            table.insert(parts, col)
        end
    end
    
    return #parts > 0 and table.concat(parts, ", ") or nil
end

-- Helper function to format excerpts
function formatExcerpts(excerpts)
    if not excerpts or #excerpts == 0 then return nil end
    
    local parts = {}
    for _, excerpt in ipairs(excerpts) do
        if type(excerpt) == "table" and excerpt.srcColumn then
            local part = tostring(excerpt.srcColumn)
            if excerpt.destColumn then
                part = part .. ":" .. tostring(excerpt.destColumn)
            end
            if excerpt.namespace then
                part = part .. ":" .. tostring(excerpt.namespace)
            end
            if excerpt.charLimit then
                part = part .. ":" .. tostring(excerpt.charLimit)
            end
            if excerpt.charHardLimit then
                part = part .. ":" .. tostring(excerpt.charHardLimit)
            end
            table.insert(parts, part)
        elseif type(excerpt) == "string" then
            table.insert(parts, excerpt)
        end
    end
    
    return #parts > 0 and table.concat(parts, ", ") or nil
end

return Report