Moduli:Votes
Moduli i votimeve të qendrave të komunitetit. Lexon listat e kërkesave (pjesën të faqes së listës, me kërkesat që ajo përfshin), numëron votat e secilës kërkesë dhe tregon, sipas rregullave të procedurës, sa kohë mbetet dhe nëse votimi mund të mbyllet.
Përdoret nga:
- Moduli:WindowSections: dritaret e Kuvendit (rreshti nën titullin «Votimi» të çdo kërkese; kërkesat e mbyllura para kohe mbeten në dritare derisa të arkivohen);
- {{Vote summary}} (
{{#invoke:Votes|summary|faqja e listës|rregulli}}): tabela në krye të listave të qendrave; - arkivat e Administratës dhe të artikujve të mirë e të përkryer (
{{#invoke:Votes|archive|…}}): shifrat sipër tabelës së arkivit (shih më poshtë). - kokat e faqeve të votimeve të Administratës (
{{#invoke:Votes|count|…}}): numri i kërkesave të hapura (shih më poshtë); - Moduli:Mbyllje diskutimi ({{dmb}}): mesazhi i mbylljes, kur një votim mbyllet para kohe (shih më poshtë).
Rregullat
Rregullat janë në tabelën RULES të modulit, sipas faqeve të procedurave:
| Rregulli | Ku | Afati minimal | Kushti për mbyllje |
|---|---|---|---|
grisje |
Forumi i Grisjes | 2 javë | 3 vota në njërën anë (Grise ose Mbaje) |
kandidature |
Kandidaturat AM/AP | një javë | 3 vota në njërën anë (PËR ose KUNDËR); kalon me dy të tretat PËR |
rivleresim |
Rivlerësimet AM/AP | një javë | 3 vota me të njëjtën stampë |
zgjedhje |
Zgjedhjet dhe shkarkimet e Administratës | 15 ditë (deri në 30) | të paktën 5 PËR dhe 75% PËR |
votim |
Ofiçina | — | — (vetëm votat) |
diskutim |
Pyetje e Përgjigje, Zyra e Ankesave | — | — (pa vota: data e komentit të fundit të çdo teme dhe kur e arkivon MajavahBot, pas 14 ditësh pa komente të reja) |
Koha minimale nuk është afat mbylljeje: para saj votimi nuk mund të mbyllet («⏳ Mund të mbyllet pas 5 ditësh»); pas saj, votimi mbyllet kur plotësohen kushtet («✓ Gati për mbyllje») ose vazhdon derisa të vijnë votat që mungojnë («🗳 Pret votat»). Koha minimale numërohet nga krijimi i faqes së kërkesës: kur krijohet faqja, shablloni i kërkesës (faqja …/InputInit e procedurës) shkruan «minimalisht më data» (dhe, te Administrata, «maksimalisht më data»). Për kërkesat e hapura para 28 shtatorit 2026, datat e krijimit të faqeve janë te Moduli:Votes/dates. Kur mungojnë të dyja, koha llogaritet nga firma e parë. Këshilla e gjendjes tregon kur u hap votimi dhe nga kur mund të mbyllet. Votat numërohen në seksionin «Votimi» të kërkesës: një votë për përdorues (e fundit), pa votat e hequra me vijë. Stampat e votave dhe ridrejtimet e tyre janë në tabelën TEMPLATES.
Mbyllja para kohe
closing_check i tregon {{dmb}} nëse kërkesa në faqen e së cilës ndodhet po mbyllet para se të plotësohen kushtet e rregullit të saj: para kohës nga e cila mund të mbyllet («mbyllur para kohe»), ose pas saj, por pa votat që i duhen («mbyllur pa votat e nevojshme»); këto fjalë hyjnë në vetë mesazhin e mbylljes, bashkë me atë që mungonte. Koha e mbylljes merret nga redaktimi i fundit i faqes së kërkesës; në parapamje është çasti i parapamjes, që kushdo që e mbyll votimin ta lexojë para ruajtjes (votat dhe afati merren nga versioni i ruajtur i faqes). Kontrollohen vetëm kërkesat që janë ende në listën e tyre (tabela REQUESTS: parashtesa e faqeve të kërkesave të çdo procedure, rregulli dhe listat e saj); kërkesat e vjetra, të mbyllura e të arkivuara sipas praktikës së kohës së tyre, nuk preken. Mesazhi i një mbylljeje të tillë del me ngjyrën e paralajmërimit (qelibar) dhe me shenjën ⚠, cilido qoftë vendimi. Po ky kontroll (early_closing) shënon «⚠ Mbyllur para kohe» ose «⚠ Mbyllur pa votat e nevojshme», me po atë ngjyrë, te {{Vote summary}} dhe te dritaret e Kuvendit, ku kërkesa e mbyllur para kohe mbetet derisa të arkivohet (needing_attention). Faqet e shënuara mblidhen te Kategoria:Votime të mbyllura para kohe.
Arkivat
Që rreshtat të theksohen, rreshti Nën tabelë,
{{#invoke:Votes|archive|kandidatura|në detyrë}} vendoset brenda …-at-approved, …-at-rejected, …-at-withdrawn; adm-at-self për kandidaturat vetjake të Administratës; …-at-yes ose adm-at-duty për ata që janë ende «po»), dhe i tregon si çelësa: kur zgjidhet një shifër, rreshtat e saj theksohen dhe të tjerët zbehen; kur zgjidhet sërish, ose kur zgjidhet totali, tabela shfaqet e plotë. Parametri i parë është emri i rreshtave në shumës, i dyti emri i shifrës së fundit («në detyrë», «ende të mira»).
|- i secilit mban klasat e shifrave të tij: at-approved, at-rejected ose at-withdrawn, at-self kur kandidatura është vetjake dhe at-still kur është ende «po», p.sh. |- class="at-approved at-self at-still". Te Administrata, qelizat e kolonave «Vetjake» dhe «Në detyrë» shkruhen njësoj: po (ose adm-at-duty), jo, dhe — kur nuk vlen (stilet janë te Wikipedia:Administrata/styles.css).
archive-legend-title dhe archive-legend-items) shpjegon të gjitha kolonat e saj; stilet janë te Moduli:Votes/styles.css.
Numri i kërkesave të hapura
{{#invoke:Votes|count|faqja e listës|faqja e listës…|rule=…|emri=…|emrat=…|hapur=…}} jep me fjalë numrin e kërkesave të hapura të një ose më shumë listave, për kokat e faqeve të votimeve të Administratës: «Nuk ka asnjë kandidaturë të hapur aktualisht», «1 kandidaturë e hapur aktualisht», «3 kandidatura të hapura aktualisht». Kërkesat e mbyllura që janë ende në listë nuk numërohen. Fjalët janë ato të rregullit (parazgjedhja është zgjedhje), ose emri (njëjës), emrat (shumës) dhe hapur («e hapur» ose «i hapur»). Me shkurt=po del forma e shkurtër, që zë një rresht në kartat e një qendre: «Asnjë kandidaturë e hapur», «1 kandidaturë e hapur», «3 kandidatura të hapura», brenda një span me klasën votes-count-none ose votes-count-open (që qendra t’i theksojë kërkesat e hapura). Shembull:
{{#invoke:Votes|count|Wikipedia:Administrata/Votimet/Kandidatura për administratorë/Kandidatura vetjake|Wikipedia:Administrata/Votimet/Kandidatura për administratorë/Kandidatura të propozuara}}
--------------------------------------------------------------------------------
-- Module:Votes
--
-- The votes of the community hubs. Reads a list of requests (the part of a list page between
-- and its end, with the requests that it transcludes), counts the
-- votes of each request and tells, by the rules of its procedure, how long the vote still lasts
-- and whether it can be closed.
--
-- Used by:
-- Moduli:WindowSections, the windows of [[Wikipedia:Kuvendi]] (a line under the «Votimi» title
-- of each request; the requests closed before the conditions of their vote were met stay in the
-- windows, needing_attention in section 5);
-- {{Vote summary}} = {{#invoke:Votes|summary|
|
-- of the hubs with each request, its votes and its state;
-- {{#invoke:Votes|archive|…}}, the figures above the tables of the archives (section 7);
-- {{#invoke:Votes|count|…}}, the number of open requests in the headers of the voting pages of
-- Administrata (section 8);
-- Moduli:Mbyllje diskutimi ({{dmb}}), whose message says when a request is closed before the
-- conditions of its vote are met, in the amber of a warning (closing_check, section 5);
-- the hubs, which show the requests of their lists with {{#invoke:Votes|page|}} or
-- {{#invoke:Votes|cards|}}, so that the closing of each request knows its page
-- (section 5b).
--
-- The rules (RULES below) are those of the pages of the procedures:
-- grisje Forumi i Grisjes: at least 2 weeks, 3 votes on one side (Grise or Mbaje)
-- kandidature AM/AP candidacies: at least a week, 3 votes on one side (PËR or KUNDËR);
-- the article passes with two thirds PËR
-- rivleresim AM/AP re-evaluations: at least a week, 3 votes on one side (Mbaje or Hiqe)
-- zgjedhje Administrata elections and removals: 15 to 30 days, at least 5 PËR and 75% PËR
-- votim Ofiçina: the votes only
-- diskutim Pyetje e Përgjigje, Zyra e Ankesave: no votes; the date of the last comment of each
-- thread and when MajavahBot archives it (14 days without new comments)
--
-- The minimum time of a vote counts from the creation of the request's page. The preloads write
-- the date before which the request cannot be closed («minimalisht më …») when the page is
-- created; for the older requests, the creation dates are in Module:Votes/dates.
--------------------------------------------------------------------------------
local p = {}
local mw_text = mw.text
local mw_uri = mw.uri
--------------------------------------------------------------------------------
-- 1. Votes and rules
--------------------------------------------------------------------------------
-- The vote templates and their redirects, by kind, and the name of each kind in the tally.
-- {{Koment}} is not a vote.
p.TEMPLATES = {
per = { 'PËR', '+1' },
kunder = { 'KUNDËR', '-1', 'KUNDRA' },
asnjanes = { 'ASNJANËS', 'NEUTRAL' },
grise = { 'Të griset', 'Te griset', 'Grise voto' },
mbaje = { 'Të mbahet', 'Te mbahet', 'Mbaje', 'Mbaje voto' },
hiqe = { 'Të hiqet', 'Hiqe', 'Hiqe voto' },
pav = { 'I pavendosur', 'I pavendosur voto' },
}
p.LABELS = {
per = 'PËR', kunder = 'KUNDËR', asnjanes = 'ASNJANËS',
grise = 'Grise', mbaje = 'Mbaje', hiqe = 'Hiqe', pav = 'I pavendosur',
}
-- The rules of each kind of vote:
-- kinds: the kinds shown in the tally (with «all», those that are not used are left out)
-- min, sides: the vote can be closed once one of the sides has at least «min» votes; a side is
-- the list of the kinds that count for it
-- min_per, share: at least that many PËR votes and that share of PËR among PËR and KUNDËR
-- (share_gate: the share is a condition for closing; otherwise it is only shown)
-- days: the minimum time of the vote, for the items whose end date is not written
-- noun: the name of an item of the list, singular and plural, with «open» in agreement
-- note: the rule in words, for the tooltips
p.RULES = {
grisje = {
kinds = { 'grise', 'mbaje', 'pav' },
min = 3, sides = { { 'grise' }, { 'mbaje' } },
days = 14,
noun = { 'kërkesë', 'kërkesa', 'e hapur' },
note = 'Votimi zgjat të paktën 2 javë, derisa njëra anë të ketë të paktën 3 vota (Grise ose Mbaje).',
},
kandidature = {
kinds = { 'per', 'kunder', 'asnjanes' },
min = 3, sides = { { 'per' }, { 'kunder' } },
share = 2 / 3,
days = 7,
noun = { 'kandidaturë', 'kandidatura', 'e hapur' },
note = 'Votimi zgjat të paktën një javë, derisa njëra anë të ketë të paktën 3 vota (PËR ose KUNDËR). Artikulli kalon me dy të tretat e votave PËR.',
},
-- the procedure asks for {{Të mbahet}} and {{Të hiqet}}; {{PËR}} and {{KUNDËR}}, which some voters
-- use, do not say by themselves whether they are for keeping or for removing, so each template
-- is a side of its own: three votes with the same template
rivleresim = {
kinds = { 'mbaje', 'hiqe', 'pav', 'per', 'kunder', 'asnjanes' }, all = true,
min = 3, sides = { { 'mbaje' }, { 'hiqe' }, { 'per' }, { 'kunder' } },
days = 7,
noun = { 'rivlerësim', 'rivlerësime', 'i hapur' },
note = 'Votimi zgjat të paktën një javë, derisa njëra anë të ketë të paktën 3 vota (Të mbahet ose Të hiqet).',
},
zgjedhje = {
kinds = { 'per', 'kunder', 'asnjanes' },
min_per = 5, share = 0.75, share_gate = true,
days = 15,
noun = { 'kandidaturë', 'kandidatura', 'e hapur' },
note = 'Votimi zgjat 15 deri në 30 ditë. Kërkohen të paktën 5 vota PËR dhe 75% PËR (pa votat ASNJANËS).',
},
votim = {
kinds = { 'per', 'kunder', 'asnjanes' },
noun = { 'kërkesë', 'kërkesa', 'e hapur' },
},
-- the threads of a discussion page, which MajavahBot archives («algo = old(14d)» in its
-- configuration on the page) once they have had no new comment for «archive» days; a thread
-- without any signature has no date, and the robot leaves it
diskutim = {
archive = 14,
noun = { 'temë', 'tema', 'e hapur' },
note = 'Temat pa komente të reja për 14 ditë arkivohen nga MajavahBot.',
},
}
-- Templates that close a thread: a question answered, a request resolved or declined
-- ({{Resolved}}, {{Refused}} and their Albanian names), a candidacy or a deletion request closed
-- ({{dmb}} = {{Mbyllje diskutimi}})
p.CLOSING_TEMPLATES = { 'Resolved', 'Zgjidhur', 'Refused', 'Refuzuar', 'Dmb', 'Mbyllje diskutimi' }
local MONTHS = {
jan = 1, shk = 2, mar = 3, pri = 4, maj = 5, qer = 6,
kor = 7, gus = 8, sht = 9, tet = 10, ['nën'] = 11, dhj = 12,
}
local ZONES = { CET = 1, CEST = 2, UTC = 0, GMT = 0 }
--------------------------------------------------------------------------------
-- 2. Reading the lists
--------------------------------------------------------------------------------
-- The wikitext of a page as it is transcluded: only its ,
local STRUCK = {
{ '<[sS]>', '[sS]%s*>' },
{ '<[sS]%s[^>]*>', '[sS]%s*>' },
{ '<[dD][eE][lL][^>]*>', '[dD][eE][lL]%s*>' },
{ '<[sS][tT][rR][iI][kK][eE][^>]*>', '[sS][tT][rR][iI][kK][eE]%s*>' },
}
-- The votes of an item: the lines of its section «Votimi» (up to the next heading), without the
-- struck text; one vote for each user, the last one. Nil when the item has no such section.
function p.tally_of(lines)
local section, inside = {}, false
for _, line in ipairs(lines) do
local level, title = p.heading_of(line)
if level then
if inside then
break
end
inside = level >= 3 and mw.ustring.find(mw.ustring.lower(title), '^votim') ~= nil
elseif inside then
table.insert(section, line)
end
end
if not inside then
return nil
end
local text = table.concat(section, '\n')
for _, tag in ipairs(STRUCK) do
text = text:gsub(tag[1] .. '.-' .. tag[2], '')
end
local last, n = {}, 0
for line in mw_text.gsplit(text, '\n', true) do
local kind = vote_of(line)
if kind then
n = n + 1
last[voter_of(line) or ('#' .. n)] = kind
end
end
local counts = {}
for _, kind in pairs(last) do
counts[kind] = (counts[kind] or 0) + 1
end
return counts
end
--------------------------------------------------------------------------------
-- 4. Dates
--------------------------------------------------------------------------------
-- Days since 1970-01-01 of a date of the calendar
local function days_from_civil(y, m, d)
if m <= 2 then
y = y - 1
end
local era = math.floor(y / 400)
local yoe = y - era * 400
local doy = math.floor((153 * ((m + 9) % 12) + 2) / 5) + d - 1
local doe = yoe * 365 + math.floor(yoe / 4) - math.floor(yoe / 100) + doy
return era * 146097 + doe - 719468
end
-- A date as the preloads and the signatures write it («1 tetor 2026, 21:11 (CEST)»)
local DATE = "(%d%d?)%s+([^%s%d,%(%)']+)%s+(%d%d%d%d)%s*,?%s*(%d%d?):(%d%d)%s*%((%u+)%)"
-- The date in seconds since 1970 (UTC), or nil
local function to_time(d, mon, y, h, mi, zone)
local m = MONTHS[mw.ustring.lower(mw.ustring.sub(mon, 1, 3))]
if not m then
return nil
end
return days_from_civil(tonumber(y), m, tonumber(d)) * 86400 + tonumber(h) * 3600 + tonumber(mi) * 60
- (ZONES[zone] or 1) * 3600
end
-- The date that follows some words («minimalisht më ''1 tetor 2026, 21:11 (CEST)''»): the time and
-- the date as written
local function date_after(text, words)
local _, e = text:find(words, 1, true)
if not e then
return nil
end
local from, d, mon, y, h, mi, zone, to = text:match("^[%s']*()" .. DATE .. '()', e + 1)
local t = from and to_time(d, mon, y, h, mi, zone)
return t, t and text:sub(from, to - 1)
end
-- The creation of the page of a request (seconds since 1970, UTC), from Module:Votes/dates: the
-- older requests, whose pages do not all carry the date before which they cannot be closed (the
-- new ones get it from their preloads)
local function creation_of(page)
if not page then
return nil
end
local ok, dates = pcall(mw.loadData, 'Module:Votes/dates')
local iso = ok and dates[page]
local y, m, d, h, mi, s = (iso or ''):match('^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)')
if not y then
return nil
end
return days_from_civil(tonumber(y), tonumber(m), tonumber(d)) * 86400
+ tonumber(h) * 3600 + tonumber(mi) * 60 + tonumber(s)
end
-- The date of the first signature of an item, when its end date is not written
local function first_signature(text)
local first, first_text
for from, d, mon, y, h, mi, zone, to in text:gmatch('()' .. DATE .. '()') do
local t = to_time(d, mon, y, h, mi, zone)
if t and (not first or t < first) then
first, first_text = t, text:sub(from, to - 1)
end
end
return first, first_text
end
-- The date of the last signature of an item: the time of its last comment
local function last_signature(text)
local last
for d, mon, y, h, mi, zone in text:gmatch(DATE) do
local t = to_time(d, mon, y, h, mi, zone)
if t and (not last or t > last) then
last = t
end
end
return last
end
-- The time until a vote can be closed: «Mund të mbyllet pas 5 ditësh», «… pas 1 dite»,
-- «… pas 3 orësh». The minimum time of a vote is not a deadline: before it the vote cannot be
-- closed; after it, it goes on until it has the votes it needs.
local function time_left(seconds)
if seconds >= 86400 then
local days = math.ceil(seconds / 86400)
return 'Mund të mbyllet pas ' .. days .. (days == 1 and ' dite' or ' ditësh')
end
local hours = math.max(1, math.ceil(seconds / 3600))
return 'Mund të mbyllet pas ' .. hours .. (hours == 1 and ' ore' or ' orësh')
end
local MONTH_NAMES = { 'janar', 'shkurt', 'mars', 'prill', 'maj', 'qershor', 'korrik', 'gusht',
'shtator', 'tetor', 'nëntor', 'dhjetor' }
-- Whether a day of the calendar is in summer time (CEST) in the time of Tirana: from the last Sunday of
-- March to the last Sunday of October
local function summer_day(year, month, day)
local function last_sunday(m)
local last = os.date('!*t', os.time({ year = year, month = m + 1, day = 1, hour = 12 }) - 86400)
return last.day - (last.wday - 1)
end
return (month > 3 and month < 10)
or (month == 3 and day >= last_sunday(3))
or (month == 10 and day < last_sunday(10))
end
-- A time (seconds since 1970, UTC) as a date in words, in the time of Tirana: «2 tetor 2026»
local function date_words(t)
local d = os.date('!*t', t + 3600)
if summer_day(d.year, d.month, d.day) then
d = os.date('!*t', t + 7200)
end
return d.day .. ' ' .. MONTH_NAMES[d.month] .. ' ' .. d.year
end
-- The time of the last edit of a page (seconds since 1970, UTC); without a page, that of the page being
-- shown: in a preview, the time of the preview. {{REVISIONTIMESTAMP}} gives it in the time of the wiki
-- (that of Tirana).
local function revision_time(frame, page)
local stamp = frame:preprocess('{{REVISIONTIMESTAMP' .. (page and (':' .. page) or '') .. '}}')
local y, m, d, h, mi, s = stamp:match('^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)$')
if not y then
return nil
end
y, m, d = tonumber(y), tonumber(m), tonumber(d)
return days_from_civil(y, m, d) * 86400 + tonumber(h) * 3600 + tonumber(mi) * 60 + tonumber(s)
- (summer_day(y, m, d) and 2 or 1) * 3600
end
local function capitalize(text)
return mw.ustring.upper(mw.ustring.sub(text, 1, 1)) .. mw.ustring.sub(text, 2)
end
--------------------------------------------------------------------------------
-- 5. The state of an item
--------------------------------------------------------------------------------
-- The votes of a side
local function side_count(counts, side)
local n = 0
for _, kind in ipairs(side) do
n = n + (counts[kind] or 0)
end
return n
end
-- The state of an item of a vote, or nil when it has no «Votimi» section:
-- votes: the tally («2 PËR · 2 KUNDËR · 0 ASNJANËS · 50% PËR»)
-- status: before the minimum time is over, when the vote can be closed and what it still needs
-- («Mund të mbyllet pas 5 ditësh · duhen 3 vota në njërën anë»); after it, «Gati për mbyllje»
-- once the conditions are met, or «Pret votat» with what is still missing (nil for a vote
-- without rules)
-- kind: «ready», «wait» (the minimum time is over, votes are missing) or «time»
-- note: the tooltip (when the vote opened, from when it can be closed, and the rule)
-- deadline, when: the time from which it can be closed, and that time in words; missing: what the
-- votes still lack, as a list, and lacked: the same in the past, for a vote that was closed
-- without them (early_closing)
function p.state(lines, rule, now)
local counts = p.tally_of(lines)
if not counts then
return nil
end
now = now or os.time()
local state = { counts = counts }
local parts = {}
for _, kind in ipairs(rule.kinds) do
local n = counts[kind] or 0
if n > 0 or not rule.all then
table.insert(parts, n .. ' ' .. p.LABELS[kind])
end
end
local votes = #parts > 0 and table.concat(parts, ' · ') or '0'
local per, kunder = counts.per or 0, counts.kunder or 0
if rule.share and per + kunder > 0 then
votes = votes .. ' · ' .. math.floor(100 * per / (per + kunder) + 0.5) .. '% PËR'
end
state.votes = votes
if not (rule.min or rule.min_per) then
return state
end
local missing, lacked = {}, {}
if rule.min then
local reached = false
for _, side in ipairs(rule.sides) do
if side_count(counts, side) >= rule.min then
reached = true
end
end
if not reached then
table.insert(missing, 'duhen ' .. rule.min .. ' vota në njërën anë')
table.insert(lacked, 'duheshin ' .. rule.min .. ' vota në njërën anë')
end
end
if rule.min_per and per < rule.min_per then
local n = rule.min_per - per
table.insert(missing, n == 1 and 'mungon 1 votë PËR' or ('mungojnë ' .. n .. ' vota PËR'))
table.insert(lacked, n == 1 and 'mungonte 1 votë PËR' or ('mungonin ' .. n .. ' vota PËR'))
end
if rule.share_gate and per + kunder > 0 and per / (per + kunder) < rule.share then
table.insert(missing, 'duhen ' .. math.floor(100 * rule.share + 0.5) .. '% PËR')
table.insert(lacked, 'duheshin ' .. math.floor(100 * rule.share + 0.5) .. '% PËR')
end
-- The vote opened when the page of the request was created: the date comes from
-- Module:Votes/dates for the older requests, else from the date before which the request
-- cannot be closed («minimalisht më», written by the preload when the page was created), else,
-- for a request without either, from its first signature. The written date is the one before
-- which it cannot be closed; without it, the minimum time is counted from the opening.
local text = table.concat(lines, '\n')
local deadline, when = date_after(text, 'minimalisht më')
local latest = date_after(text, 'maksimalisht më')
local opened, how = creation_of(SOURCES[lines[1]]), ''
if not opened and deadline and rule.days then
opened = deadline - rule.days * 86400
elseif not opened and not deadline and rule.days then
opened, how = first_signature(text), ' (firma e parë)'
end
if not deadline and opened and rule.days then
deadline = opened + rule.days * 86400
end
local note
if deadline then
note = (opened and ('Hapur më ' .. date_words(opened) .. how .. '; mund') or 'Mund')
.. ' të mbyllet nga ' .. (when or date_words(deadline)) .. ' e tutje.'
end
state.deadline, state.when = deadline, deadline and (when or date_words(deadline))
state.missing, state.lacked = missing, lacked
if deadline and (now >= deadline or (latest and now >= latest)) then
if #missing == 0 or (latest and now >= latest) then
state.status, state.kind, state.ready = '✓ Gati për mbyllje', 'ready', true
else
state.status, state.kind = '🗳 Pret votat · ' .. table.concat(missing, ' · '), 'wait'
end
elseif deadline then
state.status, state.kind = '⏳ ' .. time_left(deadline - now)
.. (#missing > 0 and (' · ' .. table.concat(missing, ' · ')) or ''), 'time'
elseif #missing > 0 then
state.status, state.kind = capitalize(table.concat(missing, ' · ')), 'time'
end
state.note = mw_text.trim((note or '') .. ' ' .. (rule.note or ''))
return state
end
-- The state of a thread of a discussion page (a rule with «archive»): the date of its last comment
-- and when the robot archives it:
-- last: the date of the last comment in words («23 shtator 2026»), or nil for a thread without
-- signatures
-- status: «Arkivohet pas 9 ditësh», «Arkivohet së shpejti» once the days are over (at the
-- robot's next run), or «Pa firmë: nuk arkivohet»
-- kind: «time», «due» or «none»; note: the tooltip
function p.discussion_state(lines, rule, now)
now = now or os.time()
local last = last_signature(table.concat(lines, '\n'))
local state = { note = rule.note }
if not last then
state.status, state.kind = 'Pa firmë: nuk arkivohet', 'none'
return state
end
state.last = date_words(last)
local left = last + rule.archive * 86400 - now
if left > 0 then
local days = math.ceil(left / 86400)
state.status = 'Arkivohet pas ' .. days .. (days == 1 and ' dite' or ' ditësh')
state.kind = 'time'
else
state.status, state.kind = 'Arkivohet së shpejti', 'due'
end
state.note = 'Komenti i fundit më ' .. state.last .. '. ' .. rule.note
return state
end
-- Whether a vote was closed before its conditions were met, given the time of its closing («closed»,
-- seconds since 1970, UTC): nil when it was not, or when the vote has no conditions; else
-- kind: «time» (before the time from which it could be closed) or «votes» (after it, without the
-- votes it needs)
-- status: «mbyllur para kohe» or «mbyllur pa votat e nevojshme», for the message of the closing and
-- for the row of a summary
-- text: what was not met («votimi mund të mbyllej nga 1 tetor 2026, 21:11 (CEST) e tutje; duheshin
-- 3 vota në njërën anë»)
-- note: the rule in words
function p.early_closing(lines, rule, closed)
if not (rule.min or rule.min_per) then
return nil
end
local state = p.state(lines, rule, closed)
if not state or state.ready then
return nil
end
local parts = {}
local before = state.deadline and closed < state.deadline
if before then
table.insert(parts, 'votimi mund të mbyllej nga ' .. state.when .. ' e tutje')
end
for _, lack in ipairs(state.lacked or {}) do
table.insert(parts, lack)
end
if #parts == 0 then
return nil
end
return {
kind = before and 'time' or 'votes',
status = before and 'mbyllur para kohe' or 'mbyllur pa votat e nevojshme',
text = table.concat(parts, '; '),
note = rule.note,
}
end
-- The requests of each procedure: the prefix of their pages, the rule of their vote and the lists where
-- they sit while they are open and, once closed, until they are archived. (Voluntary resignations and
-- requests for checkusers have no vote.)
p.REQUESTS = {
{ prefix = 'Wikipedia:Forumi i Grisjes/Kërkesë për grisje/', rule = 'grisje',
lists = { 'Wikipedia:Forumi i Grisjes/Kërkesë për grisje' } },
{ prefix = 'Wikipedia:Artikuj të mirë/Kandidatura/', rule = 'kandidature',
lists = { 'Wikipedia:Artikuj të mirë/Kandidatura/Votimet' } },
{ prefix = 'Wikipedia:Artikuj të mirë/Rivlerësime/', rule = 'rivleresim',
lists = { 'Wikipedia:Artikuj të mirë/Rivlerësime/Votimet' } },
{ prefix = 'Wikipedia:Artikuj të përkryer/Kandidatura/', rule = 'kandidature',
lists = { 'Wikipedia:Artikuj të përkryer/Kandidatura/Votimet' } },
{ prefix = 'Wikipedia:Artikuj të përkryer/Rivlerësime/', rule = 'rivleresim',
lists = { 'Wikipedia:Artikuj të përkryer/Rivlerësime/Votimet' } },
{ prefix = 'Wikipedia:Administrata/Administratorë/Kandidatura/', rule = 'zgjedhje',
lists = { 'Wikipedia:Administrata/Votimet/Kandidatura për administratorë/Kandidatura të propozuara',
'Wikipedia:Administrata/Votimet/Kandidatura për administratorë/Kandidatura vetjake' } },
{ prefix = 'Wikipedia:Administrata/Burokratë/Kandidatura/', rule = 'zgjedhje',
lists = { 'Wikipedia:Administrata/Votimet/Kandidatura për burokratë/Kandidatura të propozuara',
'Wikipedia:Administrata/Votimet/Kandidatura për burokratë/Kandidatura vetjake' } },
{ prefix = 'Wikipedia:Administrata/Dorëheqje/Kandidatura/', rule = 'zgjedhje',
lists = { 'Wikipedia:Administrata/Votimet/Dorëheqje/Dorëheqje të propozuara' } },
}
-- Whether one of the lists still shows a page: its section «dritare» transcludes it
local function listed_on(page, lists)
for _, list in ipairs(lists) do
for name in p.section_text(list, 'dritare'):gmatch('{{%s*:?([^{}|#\n]-:[^{}|\n]-)%s*}}') do
local title = mw.title.new(mw_text.trim(name))
if title and title.prefixedText == page then
return true
end
end
end
return false
end
-- For {{dmb}} (Module:Mbyllje diskutimi), on the page of a request: whether the request is closed before
-- the conditions of its vote are met (see early_closing; nil when it is not). The time of the closing
-- is that of the last edit of the page; in a preview, the time of the preview, so that whoever closes
-- the vote reads it before saving. Only a request that is still on its list is checked: the
-- older ones, closed and archived under the practice of their time, are left alone. «page» is for
-- tests; the default is the page being shown.
function p.closing_check(frame, page)
local title = page and mw.title.new(page) or mw.title.getCurrentTitle()
if not title then
return nil
end
local name = title.prefixedText
for _, kind in ipairs(p.REQUESTS) do
if name:sub(1, #kind.prefix) == kind.prefix then
if not listed_on(name, kind.lists) then
return nil
end
local item = p.items_of(p.transcluded_text(title:getContent() or ''))[1]
if not item then
return nil
end
SOURCES[item[1]] = name
return p.early_closing(item, p.RULES[kind.rule], revision_time(frame, page) or os.time())
end
end
return nil
end
-- For the windows of Kuvendi, which show what still needs attention: the text of a list (after
-- inline_transclusions) without its closed threads, except, with the rule of a vote, the requests
-- closed before the conditions of their vote were met, which stay until they are archived. Also
-- returns those that stayed, by the line of their heading: what early_closing tells of each. The time
-- of a closing is that of the last edit of the page of the request.
function p.needing_attention(frame, text, rule)
local early = {}
if not (rule and (rule.min or rule.min_per)) then
return p.without_closed_threads(text), early
end
return p.without_closed_threads(text, function (thread)
local source = SOURCES[thread[1]]
early[thread[1]] = source and p.early_closing(thread, rule, revision_time(frame, source) or os.time())
or nil
return early[thread[1]] ~= nil
end), early
end
--------------------------------------------------------------------------------
-- 5b. The requests of a list as the hubs show them
--------------------------------------------------------------------------------
-- The page of a request as its transclusion shows it, with its closing template told the page it is
-- on (told_its_page, section 2). The headings keep the page they come from, for their edit links.
local function request_text(frame, title)
local content = title:getContent()
if not content then
return nil
end
local name = title.prefixedText
return frame:newChild{ title = name, args = {} }:preprocess(told_its_page(content, name))
end
-- A text with the pages of the requests that it transcludes put in place as above; the rest of it
-- is left to the parser. Also returns the number of the requests.
local function with_requests(frame, text)
local out, from, found = {}, 1, 0
for first, name, last in text:gmatch('(){{%s*:?([^{}|#\n]-:[^{}|\n]-)%s*}}()') do
local title = mw.title.new(mw_text.trim(name))
local request = title and is_request(title.prefixedText) and request_text(frame, title)
if request then
table.insert(out, frame:preprocess(text:sub(from, first - 1)))
table.insert(out, request)
from, found = last, found + 1
end
end
table.insert(out, frame:preprocess(text:sub(from)))
return table.concat(out), found
end
-- {{#invoke:Votes|cards|, ): a vote that was changed or withdrawn|bosh=
-- section «dritare»), as {{#section:|dritare}} shows them, each told its page
function p.cards(frame)
local page = mw_text.trim(frame.args[1] or '')
local text, found = with_requests(frame, p.section_text(page, 'dritare'))
if found == 0 and mw_text.trim(text) == '' then
return frame.args.bosh or ''
end
return text
end
-- {{#invoke:Votes|page|}}: a whole list page, as its transclusion shows it, with its
-- requests each told its page
function p.page(frame)
local title = mw.title.new(mw_text.trim(frame.args[1] or ''))
local content = title and title:getContent()
if not content then
return ''
end
local list = frame:newChild{ title = title.prefixedText, args = {} }
return (with_requests(list, p.transcluded_text(content)))
end
local VOTES_TIP = 'Secili përdorues numërohet një herë, me votën e tij të fundit. Votat e vijëzuara nuk numërohen.'
-- The line under the «Votimi» title of an item in the windows of Kuvendi: its votes and its status;
-- for a thread of a discussion page (Pyetje e Përgjigje, Zyra e Ankesave), under its title: the
-- date of its last comment and when it is archived. Returns the HTML (or nil), whether the item
-- is ready to be closed and the kind of its state («ready», «wait» or «time» for a vote; «time»,
-- «due» or «none» for a thread), which the tooltips of the windows count. «early» (early_closing)
-- comes with a request closed before the conditions of its vote were met: its status says so, in
-- amber («⚠ Mbyllur para kohe»), with what was not met as its tooltip, and its kind is «early».
function p.meta(lines, rule, now, early)
-- a thread of a discussion page: the date of its last comment and when it is archived
if rule.archive then
local state = p.discussion_state(lines, rule, now)
local html = '',
state.kind, mw_text.encode(state.note), state.status), false, state.kind
end
local state = p.state(lines, rule, now)
if not state then
return nil, false
end
local html = string.format('',
mw_text.encode(capitalize(early.text) .. '. ' .. (early.note or '')), capitalize(early.status)),
false, 'early'
end
if state.status then
html = html .. string.format('',
state.kind, mw_text.encode(state.note), state.status)
end
return html .. '|
-- banner of {{Request counter}} (red, or green when there are none), and under it a table with
-- the requests (their titles link to them on the page), their votes and their state. Closed
-- requests that are still on the list wait to be archived and are not counted as open; one that was
-- closed before the conditions of its vote were met says so instead, in amber («⚠ Mbyllur para kohe»,
-- «⚠ Mbyllur pa votat e nevojshme»; early_closing), and its tooltip tells what was not met. Lists of
-- more than «open» (8) requests start collapsed. An empty list shows nothing, or with «bosh=po»
-- the green banner (as {{Request counter}} did on the pages where the summary replaced it).
-- With the rule «diskutim» (Pyetje e Përgjigje, Zyra e Ankesave), the table has the date of the
-- last comment of each thread and when it is archived, instead of the votes. «emri», «emrat» and
-- «hapur» give the words of the banner (as for «count»).
function p.summary(frame)
local args = frame.args
local page = mw_text.trim(args[1] or args.page or '')
if page == '' then
local parent = frame:getParent()
page = parent and parent:getTitle() or mw.title.getCurrentTitle().prefixedText
end
local rule_name = mw_text.trim(args[2] or args.rule or '')
local rule = p.RULES[rule_name]
if not rule then
return 'Vote summary: rregull i panjohur «' .. mw_text.nowiki(rule_name)
.. '» (grisje, kandidature, rivleresim, zgjedhje, votim, diskutim).'
end
local show_empty = mw_text.trim(args.bosh or '') ~= ''
local function arg(name, default)
local value = mw_text.trim(args[name] or '')
return value ~= '' and value or default
end
local items = p.items_of(mw_text.trim(p.inline_transclusions(p.section_text(page, 'dritare'), 1)))
local styles = frame:extensionTag('templatestyles', '', { src = 'Stampa:Request counter/styles.css' })
.. frame:extensionTag('templatestyles', '', { src = 'Module:Votes/styles.css' })
local sg, pl, adj = arg('emri', rule.noun[1]), arg('emrat', rule.noun[2]), arg('hapur', rule.noun[3])
if #items == 0 then
if not show_empty then
return ''
end
return styles .. string.format(
'
',
capitalize(sg), rule.archive and 'Komenti i fundit' or 'Votat', rule.archive and 'Arkivimi' or 'Gjendja',
table.concat(rows)))
return styles .. tostring(box)
end
--------------------------------------------------------------------------------
-- 7. The figures of an archive
--------------------------------------------------------------------------------
-- {{#invoke:Votes|archive| %s%s %s %s |
-- the open requests of one or more lists, in words, for the headers of the voting pages
-- («Nuk ka asnjë kandidaturë të hapur aktualisht», «1 kandidaturë e hapur aktualisht»,
-- «3 kandidatura të hapura aktualisht»). The words are those of the rule (the default is
-- «zgjedhje»), or «emri» (singular), «emrat» (plural) and «hapur» («e hapur» or «i hapur»).
-- Closed requests that are still on a list are not counted. With «shkurt» (any value), the short
-- form for the cards of a hub, which fits on one line («Asnjë kandidaturë e hapur», «1 kandidaturë e
-- hapur», «3 kandidatura të hapura»), in a span with the class votes-count-none or votes-count-open,
-- so that the hub can make the open ones stand out.
function p.count(frame)
local args = frame.args
local rule = p.RULES[mw_text.trim(args.rule or '')] or p.RULES.zgjedhje
local function arg(name, default)
local value = mw_text.trim(args[name] or '')
return value ~= '' and value or default
end
local sg, pl, adj = arg('emri', rule.noun[1]), arg('emrat', rule.noun[2]), arg('hapur', rule.noun[3])
local open = 0
for _, page in ipairs(args) do
page = mw_text.trim(page)
if page ~= '' then
local text = p.section_text(page, 'dritare')
if mw_text.trim(text) == '' then
local title = mw.title.new(page)
text = title and p.transcluded_text(title:getContent() or '') or ''
end
for _, item in ipairs(p.items_of(p.inline_transclusions(text, 1))) do
if not p.is_closed(table.concat(item, '\n')) then
open = open + 1
end
end
end
end
if mw_text.trim(args.shkurt or '') ~= '' then
local words = open == 0 and string.format('Asnjë %s %s', sg, adj)
or open == 1 and string.format('1 %s %s', sg, adj)
or string.format('%d %s të hapura', open, pl)
return string.format('%s', open == 0 and 'none' or 'open', words)
end
if open == 0 then
return string.format('Nuk ka asnjë %s të hapur aktualisht', sg)
elseif open == 1 then
return string.format('1 %s %s aktualisht', sg, adj)
end
return string.format('%d %s të hapura aktualisht', open, pl)
end
return p
…|rule=…|emri=…|emrat=…|hapur=…|shkurt=…}}: the number of