From 6df6071a72dab85248eadf74e479d803fdc8a58c Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sat, 29 Aug 2026 22:54:29 +1200 Subject: [PATCH 01/10] Unknown author fix, 27 August 2026 --- module_citeq.lua | 2 +- module_citeq_config.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 0464a85..6774519 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -138,7 +138,7 @@ local function makelink(v, out, link, maxpos, wdl) if v.qualifiers and v.qualifiers.P1932 then label = v.qualifiers.P1932[1].datavalue.value else - label = cfg.i18n_t["unknown-author"] .. (cfg.i18n_t.trackingcats["unknown-author"] or "") + label = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper) .. (cfg.i18n_t.trackingcats["unknown-author"] or "") end maxpos = maxpos + 1 out[maxpos] = label diff --git a/module_citeq_config.lua b/module_citeq_config.lua index 1c9c2ca..a5ae4a5 100644 --- a/module_citeq_config.lua +++ b/module_citeq_config.lua @@ -7,7 +7,7 @@ settings definitions for i18n local i18n_t = { ["cite_q"] = "{{cite Q|", ["erratum"] = "(erratum)", - ["unknown-author"] = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper), +-- ["unknown-author"] = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper), ["wdq"] = "[[WDQ (identifier)|Wikidata]]", -- go through "WDQ (identifier)" redirect to reduce clutter in "What links here" and improve reverse lookup. Keep in sync with {{QID}}. ["wikisource"] = "enwikisource", ["errors"] = { From fbc531998a4abdb6e85739fb692274d8e22ec96e Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Wed, 2 Sep 2026 12:55:02 +1200 Subject: [PATCH 02/10] Thesis and retracted support sync from sandbox, 1 September 2026 --- module_citeq.lua | 51 ++++++++++++++++++++++++++++++++++++----- module_citeq_config.lua | 3 +++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 6774519..489dc74 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -7,7 +7,7 @@ local followQid = wdib._followQid local getPropertyIDs = wdib._getPropertyIDs local cite_cfg = mw.loadData ('Module:Cite/config') local cfg = mw.loadData ('Module:Cite_Q/config') -local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed +local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed -------------------------------------------------------------------------------- -- makeOrdinal needs to be internationalised along with cfg.i18n_t @@ -427,6 +427,19 @@ local function _cite_q (citeq_args) if stated_as then citeq_args.publisher = stated_as end end + -- Handle thesis args - degree, supervising institution + local is_thesis = false + if citeq_args.thesis_degree then + is_thesis = true + citeq_args.degree = citeq_args.thesis_degree + citeq_args.thesis_degree = nil + end + if citeq_args.thesis_submitted_to then + is_thesis = true + citeq_args.publisher = citeq_args.thesis_submitted_to + citeq_args.thesis_submitted_to = nil + end + if not titleforced then -- Handle subtitle. if citeq_args.title then @@ -578,6 +591,8 @@ local function _cite_q (citeq_args) citeq_args.asin = nil -- suppress ASIN if ISBN exists elseif citeq_args.journal then template = template or "journal" + elseif is_thesis then + template = template or "thesis" elseif citeq_args.website then template = template or "web" end @@ -598,6 +613,34 @@ local function _cite_q (citeq_args) citeq_args.id = id end + local opt_cat = '' + + -- Indicator if item has been retracted + local retract = '' + if citeq_args.retracted_by then + opt_cat = cfg.i18n_t.trackingcats.retracted + local retract_qid = followQid({qid = qid, props = "P5824"}) + local retract_id = string.gsub(retract_qid, 'Q', '') + local retr_params = { + citeq = 'yes', + id = frame:expandTemplate{ + title = 'QID', args = {qid = retract_id} + } + } + retr_params.doi = getValue({"P356", ps = 1, qid = retract_qid}) or nil + retr_params.doi = retr_params.doi and retr_params.doi:lower() or nil + retr_params.bibcode = getValue({"P819", ps = 1, qid = retract_qid}) or nil + retr_params.pmc = getValue({"P932", ps = 1, qid = retract_qid}) or nil + retr_params.pmid = getValue({"P698", ps = 1, qid = retract_qid}) or nil + if 'yes' == citeq_args['retracted-intentional'] then + retr_params.intentional = 'yes' + end + retract = ' ' .. frame:expandTemplate{ title = "Retracted", args = retr_params } + end + -- remove non-CS1 retraction properties + citeq_args.retracted_by = nil + citeq_args['retracted-intentional'] = nil + -- clean up any blank parameters for k, v in pairs(citeq_args) do if v == "" then citeq_args[k] = nil end @@ -632,10 +675,6 @@ local function _cite_q (citeq_args) erratumid = "" end - local opt_cat = '' - if getValue( {"P5824", ps = 1, qid = qid} ) then - opt_cat = cfg.i18n_t.trackingcats.retracted - end if getValue( {"P1366", ps = 1, qid = qid} ) then opt_cat = opt_cat .. cfg.i18n_t.trackingcats.replaced end @@ -654,7 +693,7 @@ local function _cite_q (citeq_args) title = "Cite "..template, args = citeq_args } .. cfg.i18n_t.trackingcats["unknown-template"] end - return output .. erratumid .. opt_cat + return output .. erratumid .. retract .. opt_cat end local function cite_q (frame) diff --git a/module_citeq_config.lua b/module_citeq_config.lua index a5ae4a5..efc142e 100644 --- a/module_citeq_config.lua +++ b/module_citeq_config.lua @@ -71,6 +71,9 @@ local simple_properties_t = { hdl = {id = "P1184", maxvals = 1}, -- take care of |hdl-access=? ismn = {id = "P1208", maxvals = 1}, journal = {id = "P1433", maxvals = 1}, + thesis_degree = {id = "P13338", maxvals = 1}, + thesis_submitted_to = {id = "P4101", maxvals = 1}, + retracted_by = {id = "P5824", maxvals = 1}, -- transient, non-CS1 citeseerx = {id = "P3784", maxvals = 1}, osti = {id = "P3894", maxvals = 1}, -- take care of |osti-access=? biorxiv = {id = "P3951", maxvals = 1}, From d0298adb5def513b789eba8b903ece5b3d0c0f4f Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sun, 6 Sep 2026 11:48:42 +1200 Subject: [PATCH 03/10] Use Erratum template, qidonly config lookups, 5 September 2026 --- module_citeq.lua | 59 ++++++++++++++++++++++++++++++----------- module_citeq_config.lua | 12 +++++---- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 489dc74..49d5110 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -4,7 +4,7 @@ local wdib = require('Module:WikidataIB') local getValue = wdib._getValue local _getPropOfProp = wdib._getPropOfProp local followQid = wdib._followQid -local getPropertyIDs = wdib._getPropertyIDs +--local getPropertyIDs = wdib._getPropertyIDs local cite_cfg = mw.loadData ('Module:Cite/config') local cfg = mw.loadData ('Module:Cite_Q/config') local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed @@ -157,8 +157,8 @@ returns nothing; modifies the args table ]=] local function get_name_list (nl_type, args, qid, wdl) - local propertyID = "P50" - local fallbackID = "P2093" -- author name string + local propertyID + local fallbackID if nl_type =="author" then propertyID = 'P50' -- for authors @@ -357,7 +357,15 @@ local function _cite_q (citeq_args) -- loop through list of simple properties and get their values in citeq_args for name, data in pairs(cfg.simple_properties_t) do - citeq_args[name] = getValue( {data.id, fwd = "ALL", osd = "no", noicon = "true", qid = qid, maxvals = data.maxvals, linked = data.linked, rank = data.rank or "best", citeq_args[name] } ) + if data.qidonly then + citeq_args[name] = followQid({qid = qid, props = data.id}) + if citeq_args[name] == qid then + citeq_args[name] = nil + end + end + if not citeq_args[name] then + citeq_args[name] = getValue( {data.id, fwd = "ALL", osd = "no", noicon = "true", qid = qid, maxvals = data.maxvals, linked = data.linked, rank = data.rank or "best", citeq_args[name] } ) + end if data.populate_from_journal then local publishedin = getValue( {"P1433", ps = 1, qid = qid, maxvals = 0, citeq_args[name], qual = data.id, qualsonly = 'yes'} ) citeq_args[name] = publishedin or _getPropOfProp({qid = qid, prop1 = "P1433", prop2 = data.id, maxvals = data.maxvals, ps = 1}) @@ -619,19 +627,18 @@ local function _cite_q (citeq_args) local retract = '' if citeq_args.retracted_by then opt_cat = cfg.i18n_t.trackingcats.retracted - local retract_qid = followQid({qid = qid, props = "P5824"}) - local retract_id = string.gsub(retract_qid, 'Q', '') + local retract_id = string.gsub(citeq_args.retracted_by, 'Q', '') local retr_params = { citeq = 'yes', id = frame:expandTemplate{ title = 'QID', args = {qid = retract_id} } } - retr_params.doi = getValue({"P356", ps = 1, qid = retract_qid}) or nil + retr_params.doi = getValue({"P356", ps = 1, qid = citeq_args.retracted_by}) or nil retr_params.doi = retr_params.doi and retr_params.doi:lower() or nil - retr_params.bibcode = getValue({"P819", ps = 1, qid = retract_qid}) or nil - retr_params.pmc = getValue({"P932", ps = 1, qid = retract_qid}) or nil - retr_params.pmid = getValue({"P698", ps = 1, qid = retract_qid}) or nil + retr_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args.retracted_by}) or nil + retr_params.pmc = getValue({"P932", ps = 1, qid = citeq_args.retracted_by}) or nil + retr_params.pmid = getValue({"P698", ps = 1, qid = citeq_args.retracted_by}) or nil if 'yes' == citeq_args['retracted-intentional'] then retr_params.intentional = 'yes' end @@ -668,20 +675,40 @@ local function _cite_q (citeq_args) return frame:preprocess (table.concat ({'', table.concat (expand_args, ' |') .. '}}', ''})); end - local erratumid = getPropertyIDs( { "P2507", qid = qid, fwd = "ALL", osd = "no", rank = "best", maxvals = 1 } ) - if erratumid then - erratumid = " [[d:" .. erratumid .. "|" .. cfg.i18n_t.erratum .. "]]" .. cfg.i18n_t.trackingcats.erratum - else - erratumid = "" + -- Indicator if item has corrigendum/erratum + local erratumid = "" + if citeq_args.erratum then + opt_cat = opt_cat .. cfg.i18n_t.trackingcats.erratum + local erratum_params = { + citeq = 'yes', + id = frame:expandTemplate{ + title = 'QID', args = {qid = string.gsub(citeq_args.erratum, 'Q', '')} + } + } + erratum_params.doi = getValue({"P356", ps = 1, qid = citeq_args.erratum}) or nil + erratum_params.doi = erratum_params.doi and erratum_params.doi:lower() or nil + erratum_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args.erratum}) or nil + erratum_params.pmc = getValue({"P932", ps = 1, qid = citeq_args.erratum}) or nil + erratum_params.pmid = getValue({"P698", ps = 1, qid = citeq_args.erratum}) or nil + if 'yes' == citeq_args['erratum-checked'] then + erratum_params.checked = 'yes' + end + erratumid = ' ' .. frame:expandTemplate{ title = "Erratum", args = erratum_params } end + citeq_args.erratum = nil + citeq_args['erratum-checked'] = nil - if getValue( {"P1366", ps = 1, qid = qid} ) then + if citeq_args.replaced_by then opt_cat = opt_cat .. cfg.i18n_t.trackingcats.replaced + citeq_args.replaced_by = nil end if not citeq_args.title then -- emit category link if missing title opt_cat = opt_cat .. cfg.i18n_t.trackingcats['no-label-or-title']; end + -- Disable CiteSeerX IDs for now + citeq_args.citeseerx = nil + -- render the template local lc_template = template and template:lower() or "citation" local output diff --git a/module_citeq_config.lua b/module_citeq_config.lua index efc142e..70d2667 100644 --- a/module_citeq_config.lua +++ b/module_citeq_config.lua @@ -7,7 +7,6 @@ settings definitions for i18n local i18n_t = { ["cite_q"] = "{{cite Q|", ["erratum"] = "(erratum)", --- ["unknown-author"] = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper), ["wdq"] = "[[WDQ (identifier)|Wikidata]]", -- go through "WDQ (identifier)" redirect to reduce clutter in "What links here" and improve reverse lookup. Keep in sync with {{QID}}. ["wikisource"] = "enwikisource", ["errors"] = { @@ -44,6 +43,7 @@ id = PXXX maxvals = maximum number of multiple values (0 for all) linked = "no" suppresses linking +qidonly = true - fetch the QID rather than the label string populate_from_journal = true/false determines whether to look in a journal where the source is published rank = "best", "preferred", normal, etc. determines how Wikidata ranks are treated others = true - the value for the property goes to "others" section @@ -71,10 +71,12 @@ local simple_properties_t = { hdl = {id = "P1184", maxvals = 1}, -- take care of |hdl-access=? ismn = {id = "P1208", maxvals = 1}, journal = {id = "P1433", maxvals = 1}, - thesis_degree = {id = "P13338", maxvals = 1}, - thesis_submitted_to = {id = "P4101", maxvals = 1}, - retracted_by = {id = "P5824", maxvals = 1}, -- transient, non-CS1 - citeseerx = {id = "P3784", maxvals = 1}, + thesis_degree = {id = "P13338", maxvals = 1}, -- transient, non-CS1 + thesis_submitted_to = {id = "P4101", maxvals = 1}, -- transient, non-CS1 + retracted_by = {id = "P5824", qidonly=true, maxvals = 1}, -- transient, non-CS1 + replaced_by = {id = "P1366", qidonly=true, maxvals = 1}, -- transient, non-CS1 + erratum = {id = "P2507", qidonly=true, maxvals = 1}, -- transient, non-CS1 +-- citeseerx = {id = "P3784", maxvals = 1}, osti = {id = "P3894", maxvals = 1}, -- take care of |osti-access=? biorxiv = {id = "P3951", maxvals = 1}, asin = {id = "P5749", maxvals = 1}, -- What about |asin-tld=? (WD examples resolve to .com at present, but may change) From f32596a4f869ea7db32d0113d4b18cb1fa9601bf Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sun, 13 Sep 2026 16:29:08 +1200 Subject: [PATCH 04/10] Fix date and publisher for ja, books, use Cite news, disable ISSN - fix publication date to year-only for books, journal articles - suppress publisher for news, journal articles - use P31 instance of, use Cite news for news articles - disable displaying of ISSN --- module_citeq.lua | 70 ++++++++++++++++++++++++++++++++++++++--- module_citeq_config.lua | 5 +-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 49d5110..21d07d7 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -88,7 +88,7 @@ local function makelink(v, out, link, maxpos, wdl) local sitelink = mw.wikibase.getSitelink(qnumber) if qnumber == "Q2818964" then sitelink = nil end -- suppress link to "Various authors" if v.qualifiers and v.qualifiers.P1932 then - label = v.qualifiers.P1932[1].datavalue.value + label = v.qualifiers.P1932[1].datavalue.value -- object named as else label = mw.wikibase.getLabel(qnumber) if label then @@ -293,6 +293,50 @@ local function wrap_nowiki(str, disable) return mw.text.nowiki(str or '') end +-- returns the first string sequence of 3 or more digits in a date string, or nil +local function yearFromDate(datestr) + if datestr then + return string.match(datestr, '%d%d%d+') + end + return datestr +end + +-- Attempt to determine heuristically if we're citing a news article +local function isNewsArticle(qid, args) + local newsitems = { + 'Q5707594', -- news article + 'Q30070590', -- magazine article + -- TODO: somehow use WHERE {?item (wdt:P31|wdt:P31/wdt:P279*) wd:Q5707594.} + -- main subclasses of news article: + 'Q87849013', -- AP article + 'Q2144639', -- feature story + 'Q139172839', -- top story + 'Q18339884', -- cover story + 'Q3326038', -- critical review + 'Q4202018', -- interview + 'Q309481', -- obituary + 'Q108970988', -- column + 'Q2602337', -- op-ed + 'Q59908', -- opinion piece + } + -- most of the time a cheap lookup to a single value will work + if args['instance-of'] and in_array(args['instance-of'], newsitems) then + return true + end + -- otherwise check multiple non-deprecated values + local instance_of = mw.wikibase.getBestStatements(qid, 'P31') + if instance_of and instance_of[1] then + for k, v in pairs(instance_of) do + if v.mainsnak.snaktype == "value" and v.mainsnak.datatype == "wikibase-item" then + if in_array(v.mainsnak.datavalue.value.id, newsitems) then + return true + end + end + end + end + return false +end + -- sort sequence table whose values are key-value pairs by key local function comp_key(a, b) local param_a, enum_a = a[1]:match ('(%D+)(%d+)$'); -- get param name and enumerator from @@ -594,13 +638,28 @@ local function _cite_q (citeq_args) -- code to make a guess what template to use from the supplied parameters -- (first draft for proof-of-concept) - if citeq_args.isbn then - template = template or "book" - citeq_args.asin = nil -- suppress ASIN if ISBN exists + local pubyear = yearFromDate(citeq_args['publication-date']) + local is_news = isNewsArticle(qid, citeq_args) + if is_news then + template = template or "news" + -- some items use P123 (publisher) as the newspaper instead of P1433 (published in) + if citeq_args.publisher and not citeq_args.journal then + citeq_args.journal = citeq_args.publisher + citeq_args.publisher = nil + end elseif citeq_args.journal then template = template or "journal" + citeq_args['publication-date'] = pubyear or citeq_args['publication-date'] + -- articles can be published in books/proceedings, otherwise suppress publisher + if not citeq_args.isbn then + citeq_args.publisher = nil + end elseif is_thesis then template = template or "thesis" + elseif citeq_args.isbn then + template = template or "book" + citeq_args.asin = nil -- suppress ASIN if ISBN exists + citeq_args['publication-date'] = pubyear or citeq_args['publication-date'] elseif citeq_args.website then template = template or "web" end @@ -709,6 +768,9 @@ local function _cite_q (citeq_args) -- Disable CiteSeerX IDs for now citeq_args.citeseerx = nil + -- remove any remaining non-CS1 fields + citeq_args['instance-of'] = nil + -- render the template local lc_template = template and template:lower() or "citation" local output diff --git a/module_citeq_config.lua b/module_citeq_config.lua index 70d2667..8a53038 100644 --- a/module_citeq_config.lua +++ b/module_citeq_config.lua @@ -71,6 +71,7 @@ local simple_properties_t = { hdl = {id = "P1184", maxvals = 1}, -- take care of |hdl-access=? ismn = {id = "P1208", maxvals = 1}, journal = {id = "P1433", maxvals = 1}, + ['instance-of'] = {id = "P31", qidonly=true, maxvals = 5}, -- transient, non-CS1 thesis_degree = {id = "P13338", maxvals = 1}, -- transient, non-CS1 thesis_submitted_to = {id = "P4101", maxvals = 1}, -- transient, non-CS1 retracted_by = {id = "P5824", qidonly=true, maxvals = 1}, -- transient, non-CS1 @@ -82,7 +83,7 @@ local simple_properties_t = { asin = {id = "P5749", maxvals = 1}, -- What about |asin-tld=? (WD examples resolve to .com at present, but may change) ['article-number'] = {id = "P2322", maxvals = 1, populate_from_journal = true}, isbn = {id = "P212", maxvals = 1, populate_from_journal = true}, -- ISBN 13 - issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues? +-- issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues? -- jfm = {id = "P?", maxvals = 1}, -- Jahrbuch über die Fortschritte der Mathematik (not Zbl) -- sbn = {id = "P?", maxvals = 1}, -- Standard Book Number (predecessor of ISBN, not ICCU) -- message-id = {id = "P?", maxvals = 1}, -- Usenet message ID @@ -120,4 +121,4 @@ local simple_properties_t = { return { i18n_t = i18n_t, simple_properties_t = simple_properties_t, - } +} From 881047f7e9a1f456422c2d4169e91a0197ce79b2 Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Wed, 16 Sep 2026 15:29:57 +1200 Subject: [PATCH 05/10] Set free access indicators from P6954 qualifier values - Also use with retracted and erratum templates - Also check for free DOI registrants in CS1 config --- module_citeq.lua | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/module_citeq.lua b/module_citeq.lua index 21d07d7..0b73d54 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -8,6 +8,7 @@ local followQid = wdib._followQid local cite_cfg = mw.loadData ('Module:Cite/config') local cfg = mw.loadData ('Module:Cite_Q/config') local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed +local cs1_cfg = 'Module:Citation/CS1/Configuration' -------------------------------------------------------------------------------- -- makeOrdinal needs to be internationalised along with cfg.i18n_t @@ -337,6 +338,58 @@ local function isNewsArticle(qid, args) return false end +-- Figure out a URL access value, from the "online access status" (P6954) qualifier of an identifier +-- The [[Template:Cite journal]] docs state these should only allow "free" +-- e.g. DOI = P356 +local function getIdentifierAccess(qid, args, identifier) + if not args[identifier] then + return nil + end + + -- check DOI registrant is known free, using CS1 config + -- TODO: this should also be modelled in Wikidata + if identifier == 'doi' then + local cs1 = require(cs1_cfg) + local registrant, suffix = mw.ustring.match(args.doi:lower(), '^10%.([^/]+)/([^%s–]+)$'); + if registrant and suffix and cs1.known_free_doi_registrants_t[registrant] then + return 'free' + elseif registrant and suffix and cs1.extended_registrants_t[registrant] then + for _, incipit in ipairs(cs1.extended_registrants_t[registrant]) do + if mw.ustring.find (suffix, '^' .. incipit:lower()) then + return 'free' + end + end + end + end + + -- fetch statement values to examine qualifiers + local ids = mw.wikibase.getBestStatements(qid, cfg.simple_properties_t[identifier]['id']) + if not ids or not ids[1] then + return nil + end + if not ids[1].qualifiers or not ids[1].qualifiers.P6954 then + return nil + end + + -- Check property constraints (P2302) in definition of online access status (P6954) + -- Q66439598 = 'limited', -- partial open access + -- Q107459441 = 'registration', -- registration required + -- Q910845 = 'subscription', -- paywall + -- Q16572632 = 'subscription', -- login (see note on P6954) + local free_status = { + 'Q24707952', -- free to read + 'Q232932', -- open access + } + local access = ids[1].qualifiers.P6954[1] + if access.datatype == "wikibase-item" then + local accessQID = access.datavalue.value.id + if in_array(accessQID, free_status) then + return 'free' + end + end + return nil +end + -- sort sequence table whose values are key-value pairs by key local function comp_key(a, b) local param_a, enum_a = a[1]:match ('(%D+)(%d+)$'); -- get param name and enumerator from @@ -437,6 +490,17 @@ local function _cite_q (citeq_args) citeq_args.doi = citeq_args.doi and citeq_args.doi:lower() or nil; -- force |doi= value to be lower case + -- URL access for identifiers, from P6954 online access status + -- The [[Template:Cite journal]] docs state these should only allow "free" + citeq_args['bibcode-access'] = citeq_args['bibcode-access'] or getIdentifierAccess(qid, citeq_args, 'bibcode'); + citeq_args['doi-access'] = citeq_args['doi-access'] or getIdentifierAccess(qid, citeq_args, 'doi'); + citeq_args['hdl-access'] = citeq_args['hdl-access'] or getIdentifierAccess(qid, citeq_args, 'hdl'); + citeq_args['jstor-access'] = citeq_args['jstor-access'] or getIdentifierAccess(qid, citeq_args, 'jstor'); + -- citeq_args['ol-access'] = citeq_args['ol-access'] or getIdentifierAccess(qid, citeq_args, 'ol'); + citeq_args['osti-access'] = citeq_args['osti-access'] or getIdentifierAccess(qid, citeq_args, 'osti'); + citeq_args['ssrn-access'] = citeq_args['ssrn-access'] or getIdentifierAccess(qid, citeq_args, 'ssrn'); + citeq_args['s2cid-access'] = citeq_args['s2cid-access'] or getIdentifierAccess(qid, citeq_args, 's2cid'); + citeq_args.others = citeq_args.others or table.concat(oth, ". ") if citeq_args.others == "" then citeq_args.others = nil @@ -698,6 +762,9 @@ local function _cite_q (citeq_args) retr_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args.retracted_by}) or nil retr_params.pmc = getValue({"P932", ps = 1, qid = citeq_args.retracted_by}) or nil retr_params.pmid = getValue({"P698", ps = 1, qid = citeq_args.retracted_by}) or nil + -- check qualifiers for access=free + retr_params['doi-access'] = getIdentifierAccess(citeq_args.retracted_by, retr_params, 'doi') + retr_params['bibcode-access'] = getIdentifierAccess(citeq_args.retracted_by, retr_params, 'bibcode') if 'yes' == citeq_args['retracted-intentional'] then retr_params.intentional = 'yes' end @@ -749,6 +816,9 @@ local function _cite_q (citeq_args) erratum_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args.erratum}) or nil erratum_params.pmc = getValue({"P932", ps = 1, qid = citeq_args.erratum}) or nil erratum_params.pmid = getValue({"P698", ps = 1, qid = citeq_args.erratum}) or nil + -- check qualifiers for access=free + erratum_params['doi-access'] = getIdentifierAccess(citeq_args.erratum, erratum_params, 'doi') + erratum_params['bibcode-access'] = getIdentifierAccess(citeq_args.erratum, erratum_params, 'bibcode') if 'yes' == citeq_args['erratum-checked'] then erratum_params.checked = 'yes' end From 2c0b7f935fce9f5ce61da3dc547409adae364289 Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Mon, 21 Sep 2026 09:40:19 +1200 Subject: [PATCH 06/10] Tweak CS1 config load (once per page) --- module_citeq.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 0b73d54..65ec733 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -8,7 +8,7 @@ local followQid = wdib._followQid local cite_cfg = mw.loadData ('Module:Cite/config') local cfg = mw.loadData ('Module:Cite_Q/config') local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed -local cs1_cfg = 'Module:Citation/CS1/Configuration' +local cs1_cfg = mw.loadData('Module:Citation/CS1/Configuration') -------------------------------------------------------------------------------- -- makeOrdinal needs to be internationalised along with cfg.i18n_t @@ -349,12 +349,11 @@ local function getIdentifierAccess(qid, args, identifier) -- check DOI registrant is known free, using CS1 config -- TODO: this should also be modelled in Wikidata if identifier == 'doi' then - local cs1 = require(cs1_cfg) local registrant, suffix = mw.ustring.match(args.doi:lower(), '^10%.([^/]+)/([^%s–]+)$'); - if registrant and suffix and cs1.known_free_doi_registrants_t[registrant] then + if registrant and suffix and cs1_cfg.known_free_doi_registrants_t[registrant] then return 'free' - elseif registrant and suffix and cs1.extended_registrants_t[registrant] then - for _, incipit in ipairs(cs1.extended_registrants_t[registrant]) do + elseif registrant and suffix and cs1_cfg.extended_registrants_t[registrant] then + for _, incipit in ipairs(cs1_cfg.extended_registrants_t[registrant]) do if mw.ustring.find (suffix, '^' .. incipit:lower()) then return 'free' end From bd68a9118ed7a538b501bbd5903e8a6bcac91692 Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sun, 20 Sep 2026 11:00:25 +1200 Subject: [PATCH 07/10] Fix CS1 template loads --- module_citeq.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 65ec733..9c0fa0b 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -6,9 +6,9 @@ local _getPropOfProp = wdib._getPropOfProp local followQid = wdib._followQid --local getPropertyIDs = wdib._getPropertyIDs local cite_cfg = mw.loadData ('Module:Cite/config') -local cfg = mw.loadData ('Module:Cite_Q/config') -local cs1_module = 'Module:Citation/CS1' -- don't load the module yet, may not be needed -local cs1_cfg = mw.loadData('Module:Citation/CS1/Configuration') +local cfg = mw.loadData ('Module:Cite_Q/config/sandbox') +local cs1_module = 'Module:Citation/CS1/sandbox' -- don't load the module yet, may not be needed +local cs1_cfg = mw.loadData('Module:Citation/CS1/Configuration/sandbox') -------------------------------------------------------------------------------- -- makeOrdinal needs to be internationalised along with cfg.i18n_t From 57d21267c776c2e25029314ff5c0a36eac994437 Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sun, 20 Sep 2026 11:01:29 +1200 Subject: [PATCH 08/10] Only show ISSN when there are no article IDs --- module_citeq.lua | 9 +++++++++ module_citeq_config.lua | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/module_citeq.lua b/module_citeq.lua index 9c0fa0b..3c3530c 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -717,6 +717,15 @@ local function _cite_q (citeq_args) if not citeq_args.isbn then citeq_args.publisher = nil end + -- only display ISSN if there are no article identifiers + if citeq_args.issn then + if citeq_args.doi or citeq_args.pmid or citeq_args.pmc or citeq_args.arxiv or + citeq_args.bibcode or citeq_args.jstor or citeq_args.ssrn or citeq_args.s2cid or + citeq_args.hdl or citeq_args.biorxiv or citeq_args.osti or citeq_args.zbl or + citeq_args.jfm or citeq_args.mr then + citeq_args.issn = nil + end + end elseif is_thesis then template = template or "thesis" elseif citeq_args.isbn then diff --git a/module_citeq_config.lua b/module_citeq_config.lua index 8a53038..d3a72c3 100644 --- a/module_citeq_config.lua +++ b/module_citeq_config.lua @@ -83,7 +83,7 @@ local simple_properties_t = { asin = {id = "P5749", maxvals = 1}, -- What about |asin-tld=? (WD examples resolve to .com at present, but may change) ['article-number'] = {id = "P2322", maxvals = 1, populate_from_journal = true}, isbn = {id = "P212", maxvals = 1, populate_from_journal = true}, -- ISBN 13 --- issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues? + issn = {id = "P236", maxvals = 1, populate_from_journal = true}, -- distinguish from |eissn= for electronic issues? -- jfm = {id = "P?", maxvals = 1}, -- Jahrbuch über die Fortschritte der Mathematik (not Zbl) -- sbn = {id = "P?", maxvals = 1}, -- Standard Book Number (predecessor of ISBN, not ICCU) -- message-id = {id = "P?", maxvals = 1}, -- Usenet message ID From 5116c6ad8c461016721ce0c6a2cc6528f2d4581e Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Sun, 20 Sep 2026 11:19:27 +1200 Subject: [PATCH 09/10] Add template expansion for expression of concern notices --- module_citeq.lua | 28 +++++++++++++++++++++++++++- module_citeq_config.lua | 2 ++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/module_citeq.lua b/module_citeq.lua index 3c3530c..3b56a27 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -835,6 +835,32 @@ local function _cite_q (citeq_args) citeq_args.erratum = nil citeq_args['erratum-checked'] = nil + -- Indicator if item has an expression of concern notice (P14891) + local eoc = "" + if citeq_args['expression-of-concern'] then + opt_cat = opt_cat .. cfg.i18n_t.trackingcats['expression-of-concern'] + local eoc_params = { + citeq = 'yes', + id = frame:expandTemplate{ + title = 'QID', args = {qid = string.gsub(citeq_args['expression-of-concern'], 'Q', '')} + } + } + eoc_params.doi = getValue({"P356", ps = 1, qid = citeq_args['expression-of-concern']}) or nil + eoc_params.doi = eoc_params.doi and eoc_params.doi:lower() or nil + eoc_params.bibcode = getValue({"P819", ps = 1, qid = citeq_args['expression-of-concern']}) or nil + eoc_params.pmc = getValue({"P932", ps = 1, qid = citeq_args['expression-of-concern']}) or nil + eoc_params.pmid = getValue({"P698", ps = 1, qid = citeq_args['expression-of-concern']}) or nil + -- check qualifiers for access=free + eoc_params['doi-access'] = getIdentifierAccess(citeq_args['expression-of-concern'], eoc_params, 'doi') + eoc_params['bibcode-access'] = getIdentifierAccess(citeq_args['expression-of-concern'], eoc_params, 'bibcode') + if 'yes' == citeq_args['eoc-intentional'] then + eoc_params.intentional = 'yes' + end + eoc = ' ' .. frame:expandTemplate{ title = "Expression of concern", args = eoc_params } + end + citeq_args['expression-of-concern'] = nil + citeq_args['eoc-intentional'] = nil + if citeq_args.replaced_by then opt_cat = opt_cat .. cfg.i18n_t.trackingcats.replaced citeq_args.replaced_by = nil @@ -860,7 +886,7 @@ local function _cite_q (citeq_args) title = "Cite "..template, args = citeq_args } .. cfg.i18n_t.trackingcats["unknown-template"] end - return output .. erratumid .. retract .. opt_cat + return output .. erratumid .. eoc .. retract .. opt_cat end local function cite_q (frame) diff --git a/module_citeq_config.lua b/module_citeq_config.lua index d3a72c3..bd209f0 100644 --- a/module_citeq_config.lua +++ b/module_citeq_config.lua @@ -24,6 +24,7 @@ local i18n_t = { ["default"] = "th" }, ["trackingcats"] = { + ['expression-of-concern'] = "[[Category:Cite Q - cites a work with an expression of concern notice]]", ["erratum"] = "[[Category:Cite Q - cites a work with an erratum]]", ["missing-wikidata"] = "[[Category:Articles with missing Wikidata information]]", -- TODO: point to a {{cite q}}-specific category; this one is specific to Module:WikidataIB ['no-label-or-title'] = '[[Category:Cite Q - missing label or title]]', @@ -77,6 +78,7 @@ local simple_properties_t = { retracted_by = {id = "P5824", qidonly=true, maxvals = 1}, -- transient, non-CS1 replaced_by = {id = "P1366", qidonly=true, maxvals = 1}, -- transient, non-CS1 erratum = {id = "P2507", qidonly=true, maxvals = 1}, -- transient, non-CS1 + ['expression-of-concern'] = {id = "P14891", qidonly=true, maxvals = 1}, -- transient, non-CS1 -- citeseerx = {id = "P3784", maxvals = 1}, osti = {id = "P3894", maxvals = 1}, -- take care of |osti-access=? biorxiv = {id = "P3951", maxvals = 1}, From ebba5e8e7febcf5c5acd1c3ae6508caa13c38a98 Mon Sep 17 00:00:00 2001 From: Jonathan Harker Date: Mon, 21 Sep 2026 18:33:10 +1200 Subject: [PATCH 10/10] Fetch given/family names from author, editor, or qualifiers on name strings --- module_citeq.lua | 109 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/module_citeq.lua b/module_citeq.lua index 3b56a27..df49ab0 100644 --- a/module_citeq.lua +++ b/module_citeq.lua @@ -88,8 +88,16 @@ local function makelink(v, out, link, maxpos, wdl) local qnumber = v.mainsnak.datavalue.value.id local sitelink = mw.wikibase.getSitelink(qnumber) if qnumber == "Q2818964" then sitelink = nil end -- suppress link to "Various authors" + + -- Last name, first name(s) + local last_name = getValue({"P734", qid=qnumber, maxvals=1, ps=2}) + local first_names = getValue({"P735", qid=qnumber, maxvals=0, ps=2}) + if first_names then + first_names = mw.text.trim(first_names:gsub(', ', ' ')) + end + if v.qualifiers and v.qualifiers.P1932 then - label = v.qualifiers.P1932[1].datavalue.value -- object named as + label = v.qualifiers.P1932[1].datavalue.value -- "object named as" else label = mw.wikibase.getLabel(qnumber) if label then @@ -100,14 +108,22 @@ local function makelink(v, out, link, maxpos, wdl) end local position = maxpos + 1 -- Default to 'next' author. -- use P1545 (series ordinal) instead of default position. - if v["qualifiers"] and v.qualifiers["P1545"] and v.qualifiers["P1545"][1] then - position = tonumber(v.qualifiers["P1545"][1].datavalue.value) + if v.qualifiers and v.qualifiers.P1545 and v.qualifiers.P1545[1] then + position = tonumber(v.qualifiers.P1545[1].datavalue.value) end maxpos = math.max(maxpos, position) if sitelink then -- just the plain name, -- but keep a record of the links, using the same index - out[position] = label + if last_name and first_names then + out[position] = { + label = label, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = label + end link[position] = sitelink else if wdl then @@ -120,24 +136,70 @@ local function makelink(v, out, link, maxpos, wdl) ) else -- no Wikidata links wanted, so just give the plain label - out[position] = label + if last_name and first_names then + out[position] = { + label = label, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = label + end end end elseif v.mainsnak.datatype == "string" then local position = maxpos + 1 -- Default to 'next' author. -- use P1545 (series ordinal) instead of default position. - if v["qualifiers"] and v.qualifiers["P1545"] and v.qualifiers["P1545"][1] then - position = tonumber(v.qualifiers["P1545"][1].datavalue.value) + if v.qualifiers and v.qualifiers.P1545 and v.qualifiers.P1545[1] then + position = tonumber(v.qualifiers.P1545[1].datavalue.value) end maxpos = math.max(maxpos, position) - out[position] = v.mainsnak.datavalue.value + + -- Names from "author given names" and "author last names" qualifiers + local last_name + local first_names + if v.qualifiers and v.qualifiers.P9688 and v.qualifiers.P9688[1] then + last_name = v.qualifiers.P9688[1].datavalue.value + end + if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then + first_names = mw.text.trim(v.qualifiers.P9687[1].datavalue.value:gsub(', ', ' ')) + end + + if last_name and first_names then + out[position] = { + label = v.mainsnak.datavalue.value, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = v.mainsnak.datavalue.value + end else -- not a wikibase-item or a string! end else - -- code here if we want to return something when author is "unknown" + -- try the "object named as" qualifier, otherwise use the unknown author item if v.qualifiers and v.qualifiers.P1932 then - label = v.qualifiers.P1932[1].datavalue.value + label = v.qualifiers.P1932[1].datavalue.value -- "object named as" + + -- Names from "author given names" and "author last names" qualifiers + local last_name + local first_names + if v.qualifiers and v.qualifiers.P9688 and v.qualifiers.P9688[1] then + last_name = v.qualifiers.P9688[1].datavalue.value + end + if v.qualifiers and v.qualifiers.P9687 and v.qualifiers.P9687[1] then + first_names = mw.text.trim(v.qualifiers.P9687[1].datavalue.value:gsub(', ', ' ')) + end + if last_name and first_names then + out[position] = { + label = label, + last = last_name or nil, + first = first_names or nil, + } + else + out[position] = label + end else label = mw.wikibase.getLabel("Q4233718"):gsub("^%l", mw.ustring.upper) .. (cfg.i18n_t.trackingcats["unknown-author"] or "") end @@ -235,15 +297,27 @@ local function get_name_list (nl_type, args, qid, wdl) end table.sort(keys) -- as they might be out of order for i, k in ipairs(keys) do - out[k] = out[k]:gsub (''', '\''); -- prevent cs1|2 multiple names categorization; replace html entity with the actual character - mw.log(i .. " " .. k .. " " .. (out[k])) + local label + if out[k].label then + label = out[k].label + else + label = out[k] + end + label = label:gsub(''', '\''); -- prevent cs1|2 multiple names categorization; replace html entity with the actual character + mw.log(i .. " " .. k .. " " .. label) if args[nl_type .. i] then -- name gets overwritten -- pull corresponding -link only if overwritten name is same as WD name - if link[k] and (args[nl_type .. i] == out[k]) then + if link[k] and (args[nl_type .. i] == label) then args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or link[k] -- author-linkn or editor-linkn end else -- name does not get overwritten, so pull name from WD - args[nl_type .. i] = out[k] + if out[k].first and out[k].last then + -- First/last names were found, so add them + args[nl_type .. '-last' .. i] = args[nl_type .. '-last' .. i] or out[k].last + args[nl_type .. '-first' .. i] = args[nl_type .. '-first' .. i] or out[k].first + else + args[nl_type .. i] = label + end if link[k] then args[nl_type .. '-link' .. i] = args[nl_type .. '-link' .. i] or link[k] -- author-linkn or editor-linkn end @@ -273,11 +347,12 @@ end -- gets the language codes of a Wikidata entry as a table local function _lang_code(qid) - local lc = _getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P424", ps = 1} ) + -- P407 "language of work or name" + local lc = _getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P424", ps = 1} ) -- "Wikimedia language code" if lc then return mw.text.split( lc, "[, ]+" ) end - lc = _getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P218", ps = 1} ) + lc = _getPropOfProp( {qid = qid, prop1 = "P407", prop2 = "P218", ps = 1} ) -- "ISO 639-1 code" if lc then return mw.text.split( lc, "[, ]+" ) end - return _getLangOfProp(qid, "P1476") + return _getLangOfProp(qid, "P1476") -- "title" end local function lang_code(frame) return table.concat(_lang_code(frame.args.qid or mw.text.trim(frame.args[1] or "")), ", ")