Модуль:Ballot/temp

Материал из Тептар — свободной энциклопедии
Перейти к навигации Перейти к поиску

local p = {}
local trim = mw.text.trim
local green90, green85, green80, green75, green70 = "#d5fdf4", "#d3fcf3", "#b6f4e7", "#9aebd9", "#7fe3cd"
local red90, red85, red80, red75, red70 = "#fee7e6", "#ffe9e8", "#fbd3d2", "#f8bfbd", "#f4a8a6"
local yellow90, yellow85, yellow80, yellow75, yellow70 = "#fef6e7", "#fff7e8", "#fff1d4", "#ffeac0", "#ffe5ac"
--[[
Процесс работы:
1) Устанавливается список кандидатов
1.1) Список кандидатов ищется на разных страницах
1.2) В случае ошибок сообщения о них передаются в таблице и потом группируются
2) Устанавливается список избирателей
2.1) нужна заранее подготовленная таблица к выборам, без неё нет автоматической проверки критериев
2.1.1) TODO - если вообще нет такой таблицы, пропускать всех так и писать ошибку
2.2) принимаются команды с комментариями от бюрократов о необходимости учесть кого-то
        BUG - необходимо переписать, чтобы была возможность указать по одному пользователю много разных частичных ограничений
3) Установление списка голосов
3.1) vote_listing, грабит страницы с голосами при помощи pooling
3.2) compute переписывает голоса в таблицу допущенных, добавляя в таблицу err списки исключённых
3.3) сортировка кандидатов согласно ТП:ВАК 
        TODO - сделать строку серой, пока не перейдена граница 20 голосов
3.4) формируется итоговая таблица
3.4.1) записывается заранее заданная шапка
3.4.2) на основании valid_votes функция line_format записывает строки
3.4.3) сообщения об ошибках группируются и записываются самые важные вместе с подвалом таблицы
		TODO - необходимо компактно отображать ещё больше информации
3.5) выдаётся итоговый результат

Таблицы
candidatrue[candidate] - (ключ - ник кандидата, значение true/nil в зависимости от того есть ли о нём запись)

candidates[N] - (без ключа, содержит ники кандидатов)
err[N] - (без ключа, {тип ошибки текстом; группа ошибки; серьёзность ошибки: 1 - критическая, 2 - серьёзная, 3 - мелкая, 4 - не ошибка; текст ошибки}) 

vote_table[candidate] = votes[voter] {{nick,bool_pro,bool_contra,time}, }

exceptions[user] = {is_exception,in_general,allow,prevent,comment,candidate} --старая форма
exceptions[user] = { {is_exception,in_general,allow,prevent,comment,candidate}, ...}
--]]
-- local mw = mw or {} -- для уменьшения количества ошибок от локального дебаггера
-- local frame 		-- frame из p.open_vote, чтобы не волноваться о доступе из любой функции
					-- вызов функцией не переданного в неё аргумента приводит к [[Замыкание (программирование)|замыканию]], которое тут не нужно
					 
local monthlang = {"января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"}
local month_to_num = {["января"]=1,["февраля"]=2,["марта"]=3,["апреля"]=4,["мая"]=5,["июня"]=6,
	["июля"]=7,["августа"]=8,["сентября"]=9,["октября"]=10,["ноября"]=11,["декабря"]=12,["-"]=""}
local table_start, table_end = '<table class="wikitable sortable" width=33%><tr style="text-align:center"><th class="headerSortDown">#</th><th>Кандидат</th><th>+</th><th>−</th><th>Σ</th><th>%</th></tr>', "</table>"
local err_start, err_end = '<tr class="sortbottom" style="text-align:center"><td colspan=6><table class="mw-collapsible mw-collapsed"><tr><th>Дополнительная информация&nbsp;</th></tr><tr><td>','</td></tr></table></td></tr>'
local err_type = {["no data"]	="Нет данных:",	["clash"]	="Расхождения:", ["vot_ers"]	="Исключения:"}
	
-- функция для проверки, содержит ли массив запрашиваемое значение
local function is_in_list ( var, list )
	for i=1, #list do
		if var == list[i] then
			return true
		end
	end
    return false
end

-- функция для подсчёта элементов в массиве
local function n_list (tab)
	if type(tab) ~= "table" then
		return 0
	end
	local i = 0
	for k,v in pairs(tab) do
		i = i + 1
	end
	return i
end

-- граббинг данных со страниц с голосами
local function pooling (content, plus, votes)
	if not content or #content < 2 then return end
	votes = votes or {}
	for line in string.gmatch(content, "[^\n]+") do
		--при моментальном переголосовании в течении минуты модуль не может определить позднейший голос
	    local user, nick, h, m, d, mon, y = string.match(line, "^#%s%[%[user:(.-)|(.-)%]]%s(%d%d):(%d%d),%s(.-)%s(.-)%s(.-)%s%(UTC%)")
	    if user then
	      local date = os.time{year=y, month=month_to_num[mon], day=d, hour=h, min=m}
	      votes[user] = votes[user] or {}
	      table.insert(votes[user], {
	      	nick,		-- user == voter == nick
	        plus,		-- pro
	        not plus,	-- contra
	        date})		-- date
	    end
	end
  return votes
end

-- =p.dress("Кандидат",true,false,"причина")
local function dress (candidate,pro,contra,comm)
	return "<span style='font-size:90%' class='ts-comment-commentedText' title='" .. candidate .. (comm and " (" or "") .. (comm or "") .. (comm and ")" or "") .. "'>" .. (pro and "+" or (contra and "−" or "?")) .. "</span>"
end
-- TODO подсчёт результатов на заданную дату
-- отмены голосов бюрократами будут вневременные и не повлияют на динамику
local function compute (err,vote_table,electoratrue,exceptions,date)
	if not vote_table then return nil end
	date = date or os.time()
	local valid_votes = {}
	for candidate, votes in pairs(vote_table) do
		valid_votes[candidate] = valid_votes[candidate] or {}
--		mw.log("== " .. candidate .. " ==")
		for voter, vote in pairs(votes) do
			err[votes] = err[votes] or {}
			err[votes][candidate] = err[votes][candidate] or {}
			err[votes][candidate]["change"] = #vote - 1
			valid_votes[candidate][voter] = valid_votes[candidate][voter] or {}
			local min_date, pro, contra = 0, false, false
			-- local max_date = 17179869184
			table.sort(vote,function(a,b) return a[4]>b[4] end)
			pro, contra = vote[1][2], vote[1][3]
			if #vote ~= 1 then
				table.insert(err,{"vot_ers",voter,"<small><i>change: </i></small>",dress(candidate,false,false,#vote)})
			end
--[[		for i,subvote in ipairs(vote) do
				-- subvote[N] {nick,bool_pro,bool_contra,time}
				if subvote[1] ~= voter then
					table.insert(err,{"clash",candidate,voter,"(" .. subvote[1] .. ")"})
					break
				end
				if #vote == 1 then
					pro, contra = subvote[2], subvote[3]
				elseif subvote[4] > min_date then
					table.insert(err,{"vot_ers",voter,"<small><i>change: </i></small>",dress(candidate,false,false,#vote)})

					pro, contra, v_date, change = subvote[2], subvote[3], subvote[4], true
				end
			end
--]]
			if not electoratrue[voter] then
				table.insert(err,{"vot_ers",voter,"<small><i>activity: </i></small>",dress(candidate,pro,contra)})
			end
			valid_votes[candidate][voter] = {electoratrue[voter] or false, pro, contra}
			if exceptions[voter] and type(exceptions[voter]) == "table" then
				for _, ex in ipairs(exceptions[voter]) do
					-- убрал проверку ex[1], т.к. он пока всегда true
					if ex[2] or ex[7] == candidate then
						if ex[3] then
							valid_votes[candidate][voter][1] = true
							table.insert(err,{"vot_ers",voter,"<small><i>allowed: </i></small>",dress(candidate,pro,contra,ex[5] and ex[6] or nil)})
						elseif ex[4] then
						--	mw.log(voter .. " - " .. candidate)
							valid_votes[candidate][voter][1] = false
							table.insert(err,{"vot_ers",voter,"<small><i>restricted: </i></small>",dress(candidate,pro,contra,ex[5] and ex[6] or nil)})
						end
					end
				end
			end
			-- exceptions[voter] {{is_exception,in_general,allow,prevent,bool_comment,comment,candidate}, ...}
			-- err[N] {txt_type; txt_group; txt_subgroup; txt_comment}
			-- valid_votes[candidate][voter]{[1]= bool_valid, [2]=bool_pro, [3]=bool_contra}
		end
	end
	return err, valid_votes
end

local function reform (err, valid_votes)
	local pre_result, pre_sort = {},{}
	for candidate, votes in pairs(valid_votes) do
		local count_sup,count_opp = 0,0
		for voter, vote in pairs(votes) do
			if vote[1] and vote[2] then
				count_sup = count_sup + 1
			elseif  vote[1] and vote[3] then
				count_opp = count_opp + 1
			else
--				mw.log(candidate .. ">" .. voter .. ">("..tostring(vote[1])..","..tostring(vote[2])..","..tostring(vote[3])..")")
			end
		end
	
		local count_tot = count_sup + count_opp
		local percent = count_tot == 0 and 0 or (count_sup * 100 / count_tot)
		pre_result[candidate] = {count_sup,count_opp,count_tot,percent}
		-- сортировка по голосам за, но те кто набирает процент - выше
		local sort_index
--		mw.log(candidate .. " - " .. percent .. "(" .. tostring(percent > (200/3)).. ")")
		if percent >= (200/3) then
			sort_index = 1000000 + 1000 * count_sup - count_opp
		else
			sort_index = 100 * (100/3 + percent) + count_sup
		end
--		mw.log(sort_index)
		table.insert(pre_sort,{candidate, sort_index})   
	end
--	mw.logObject(pre_sort)
	table.sort(pre_sort, function(a,b) return (a[2] == b[2]) and (a[1] < b[1]) or (a[2]>b[2]) end)
	return err, pre_result, pre_sort
end

-- valid_votes[candidate][voter]{[1]= bool_valid, [2]=bool_pro, [3]=bool_contra}
-- line_format(i, cand[1], pre_result[cand[1]], arb_page))
local function line_format (i, candidate, c_res, arb_page)
	local count_sup,count_opp,count_tot,percent = c_res[1], c_res[2], c_res[3], c_res[4]
	local passing = percent > 66.66
	local second = percent >= 50
	-- нужны даты голосования и автоматический вывод то одних, то других ссылок
	local main_arb_page = string.gsub( arb_page, "%/Голосование", "")
    local tr = mw.html.create( 'tr' )
	tr	:css( 'background', passing and green80 or (second and yellow80 or red80) ) 
		:tag( 'td' ):css('text-align','right'):wikitext( (passing and '' or '<small>') .. i .. (passing and '' or '</small>')):done()
		:tag( 'td' ):css('text-align','center'):css('white-space','nowrap'):wikitext( table.concat{"[[".. arb_page .."/Голоса#", candidate, '|', candidate, "]] ([[", main_arb_page,"/Форум/Кандидаты#", candidate,"|обс.]])"}):done()
		:tag( 'td' ):css('text-align','right'):wikitext( count_sup ):done()
		:tag( 'td' ):css('text-align','right'):wikitext( count_opp ):done()
		:tag( 'td' ):css('text-align','right'):wikitext( count_tot ):done()
		:tag( 'td' ):css('text-align','right'):wikitext( (string.gsub( mw.ustring.format("%.2f&nbsp;%%", percent), "%.", ","))):done()
	return tostring( tr )
end

local function vote_listing (err,vote_table,arb_page,candidate)
    local pattern = "\n#[^#*:][^\n]+"; -- подсчёт нумерованных списков
    local pagepointer_sup=mw.title.new(arb_page .. '/+/' .. candidate, '')
    local pagepointer_opp=mw.title.new(arb_page .. '/-/' .. candidate, '')
    local text_sup=pagepointer_sup.getContent(pagepointer_sup)
    local text_opp=pagepointer_opp.getContent(pagepointer_opp)
    
	local votes = {}
	votes = pooling(text_sup, true, votes)
	local pro_votes = n_list(votes)
--	mw.log(candidate .. " + " .. pro_votes)
	votes = pooling(text_opp, false, votes)
	local opp_votes = 1+ n_list(votes) - pro_votes
--	mw.log(candidate .. " - " .. opp_votes)
	local votes = votes or {} -- хак для отображения пустой таблицы
	if not votes then return err, vote_table end
	vote_table[candidate] = votes
	err[votes] = err[votes] or {}
	err[votes][candidate] = err[votes][candidate] or {}
	err[votes][candidate]["raw_pro"] = pro_votes
	err[votes][candidate]["raw_opp"] = opp_votes

	return err, vote_table
end

-- для сложения таблиц кандидатов
local vlist={}
function vlist.__add (tru1,tru2)
	if not tru1 or not tru2 
	or type(tru1) ~= "table" or type(tru2) ~= "table"
	then return end
	
	local tru_list = {}
	for key,bool in pairs(tru1) do
		tru_list[key] = bool
	end	
	for key,bool in pairs(tru2) do
		tru_list[key] = bool
	end	

	return tru_list
end

local nomination = function(page)
	local nomination_page_text = page:getContent() or ""
	local block_pattern = "%{%{%ВАРБ:строка|1=([^\n]+)\n|2=([^\n]+)\n|3=([^\n]+)\n|4=([^\n]+)\n|5=([^\n]+)\n|6=([^\n]+)\n%}%}"
	local candidates, candidatrue = {}, {}
	setmetatable(candidatrue,vlist)
	for candidate, submitter, consent, refusal, admission, non_admission in nomination_page_text:gmatch(block_pattern) do
		candidate, consent, admission =
			trim(candidate), trim(consent), trim(admission)
		no_consent = table.concat{"<!--           Согласие (",candidate,") -->"}
		no_admission = table.concat{"<!--           Бюрократ (",candidate,") -->"}
		if not (no_consent == consent) and consent:find("(UTC)") then
			candidatrue[candidate] = true
			table.insert(candidates,candidate)
		end
	end
	return candidates, candidatrue
end

local function line_processor(raw_text_candid,patt_string,patt_candid,patt_candid_end,position)
	local candidates, candidatrue = {}, {}
	setmetatable(candidatrue,vlist)
	for line in raw_text_candid:gmatch("[^\n]+") do
    	if string.match( line, patt_string ) then
    		local candidate_text = string.match( line, patt_candid)
    		local pos0, pos1 = string.find(candidate_text,patt_candid_end)
    		local candidate = string.sub(candidate_text, position, pos0 - 1)
    		if candidate ~= "" and candidatrue[candidate] ~= true then 
    			candidatrue[candidate] = true
    			table.insert(candidates, candidate)
    		end
    	end
	end
	return candidates, candidatrue
end

-- =p.caret({},"Тептар:Выборы арбитров/Лето 2021/Голосование/Исключения")
local caret = function (err, page)
	local raw_text = mw.title.new(page):getContent() or ""
	if #raw_text < 2 then
		table.insert(err,{"no data","bureaucrat panel","",'[['.. page ..'|/Исключения]]'})
		return err, {}
	end
	local content, exceptions = false, {}
	
	for line in raw_text:gmatch("[^\n]+") do
        if content then
	-- TODO exceptions[voter] {{is_exception,in_general,allow,prevent,bool_comment,comment,candidate}, ... }
	--					"*"		true		 true		true	false	""
	--					"*"		true		 false		false	true	"отказался" "Candid"
    		local line_data = string.match( line, "%*%s(.+)")
    		local _,n = line_data:gsub("|","")
            if 		n == 0 then
				local voter_nick = mw.text.trim(line_data)
			--	mw.log("0 " .. voter_nick)
                exceptions[voter_nick] = exceptions[voter_nick] or {}
				table.insert(exceptions[voter_nick],{true,true,true,false})
            elseif 	n == 1 then
                local voter, comment = mw.ustring.match(line_data,"([^|]+)|([^|]+)")
                voter = mw.text.trim(voter)
                comment = mw.text.trim(comment)
            --  mw.log("1 " .. voter .. " - " .. comment)
                exceptions[voter] = exceptions[voter] or {}
				table.insert(exceptions[voter],{true,true,false,true,true,comment})
            elseif 	n == 2 then
                local voter, candidate, comment = mw.ustring.match(line_data,"([^|]+)%s?|%s?([^|]+)|([^|]+)")
                voter = mw.text.trim(voter)
                candidate = mw.text.trim(candidate)
                comment = mw.text.trim(comment)
            --  mw.log("1 " .. voter .. " - " .. comment .. " - " .. candidate)
                exceptions[voter] = exceptions[voter] or {}
				table.insert(exceptions[voter],{true,false,false,true,true,comment,candidate}) 
            else
                table.insert(err,{"clash","bureaucrat panel","",line_data})
    		end
		elseif string.match( line, ".*</noinclude>" ) then
    		content = true
    	end

	end
--	mw.logObject(exceptions)
	return err, exceptions
end

-- =p.exclamat(mw.title.new("Тептар:Выборы арбитров/Лето 2021/Голосование/!"))
local function exclamat (page)
	return line_processor(page:getContent() or "","====.+}}","====.+}}","{{",8)
end

-- =p.ampersan(mw.title.new("Тептар:Выборы арбитров/Лето 2021/Голосование/&"))
local function ampersan (page)
	return line_processor(page:getContent() or "","%|[^|]+%|%|%[%[.*","%|[^|]+%|%|%[%[","||%[%[",2)
end

-- =p.allvotes(mw.title.new("Тептар:Выборы арбитров/Лето 2021/Голосование/Голоса"))
local function allvotes (page)
	return line_processor(page:getContent() or "","%*%s%[%[#[^|]+|[^%]]+%]%]","%*%s%[%[#[^|]+|[^%]]+%]%]","|",6)
end

-- =p.w_quarry(mw.title.new("Тептар:Выборы арбитров/Лето 2021/Избиратели"))
local function w_quarry (page)
	local electorate, electoratrue = line_processor(page:getContent() or "","|[^|]+||%d+||%d+||%d+","^|[^|]+||","||",2)
	if not electorate[1] then
		local raw_text = page:getContent()
		-- TODO сообщение об ошибке
		if not raw_text then return {}, {} end
		for line in raw_text:gmatch("[^\n]+") do
			if line ~= "" and electoratrue[line] ~= true then 
    			electoratrue[line] = true
    			table.insert(electorate, line)
    		end
		end
	end
--	mw.logObject(electoratrue)
--	mw.logObject(electorate)
	return electorate, electoratrue
end

-- =p.elec_listing({},"Тептар:Выборы арбитров/Лето 2021/Голосование")
-- =p.elec_listing({},"Шаблон:Результат выборов арбитров")
local function elec_listing (err, arb_page)
	local page_1, page_2, _ = mw.ustring.match(arb_page,"([^/]+)/([^/]+)/([^/]+)")
	local page_name
	if not page_1 then
		page_name = table.concat({page_1 or arb_page,"Избиратели"},"/")
	else
		page_name = table.concat({page_1,page_2,"Избиратели"},"/")
	end
	local page = mw.title.new(page_name)
	local electorate, electoratrue = w_quarry (page)
	if not electorate[1] then 
		table.insert(err,{"no data","elec","","[[" .. page_name .. "|/Избиратели]]"})
	end

	return err, electoratrue
end

local function merge(candidates,err,candidatrue,mark,candidates_merge)
	for key,bool in pairs(candidatrue) do
		if not is_in_list(key,candidates_merge) then
			table.insert(err,{"no data",mark,"",key})
		elseif not is_in_list(key,candidates) then
			table.insert(candidates,key)
		end
	end
	return candidates,err
end

-- =p.cand_listing("","Тептар:Выборы арбитров/Лето 2021/Голосование")
local function cand_listing (err, arb_page)
	local candidates, candidatrue = {}, {}
	local nom_page = string.gsub( arb_page, "%/Голосование", "/Выдвижение")
	
--  local pagep_candid_excl = mw.title.new(arb_page .. '/!')
--  local pagep_candid_ampe = mw.title.new(arb_page .. '/&')
    local pagep_candid_allvotes = mw.title.new(arb_page .. '/Голоса')
    local pagep_candid_nom = mw.title.new(nom_page)
    
--  local candidates_excl, candidatrue_excl = exclamat(pagep_candid_excl)
--	local candidates_ampe, candidatrue_ampe = ampersan(pagep_candid_ampe)
	local candidates_allvotes, candidatrue_allvotes = allvotes(pagep_candid_allvotes)
	local candidates_nom, candidatrue_nom = nomination(pagep_candid_nom)
--	if not candidates_excl[1] then 
--		table.insert(err,{"no data","service page","",'[['.. tostring(pagep_candid_excl) ..'|/!]]'}) end
--	if not candidates_ampe[1] then
--		table.insert(err,{"no data","allpages changes","",'[['.. tostring(pagep_candid_ampe) ..'|/&]]'}) end
	if not candidates_allvotes[1] then 
		table.insert(err,{"no data","all votes","",'[['.. tostring(pagep_candid_allvotes) ..'|/Голоса]]'}) end
	if not candidates_nom[1] then 
		table.insert(err,{"no data","nomination","",'[['.. tostring(pagep_candid_nom) ..'|/Выдвижение]]'}) end

	candidatrue = candidatrue_allvotes + candidatrue_nom

	local err_spec = {}
--	candidates,err_spec = merge(candidates,err_spec,candidatrue,"service page",candidates_excl)
--	candidates,err_spec = merge(candidates,err_spec,candidatrue,"allpages changes",candidates_ampe)
	candidates,err_spec = merge(candidates,err_spec,candidatrue,"all votes",candidates_allvotes)
	candidates,err_spec = merge(candidates,err_spec,candidatrue,"all votes",candidates_nom)

	-- TODO нужна более чёткая проверка наличия расхождений
	if not candidates[1] then
		for _, er in ipairs(err_spec) do
			table.insert(err,er)
		end
	end

    return err, candidates
end

-- todo - сообщения об ошибках, исключение голосов
-- функция для работы через {{Результат выборов арбитров}}
-- =p.open_vote(mw.getCurrentFrame():newChild{title="Тептар:Выборы арбитров/Лето 2021/Голосование",args={"Тептар:Выборы арбитров/Лето 2021/Голосование"}})

function p.open_vote(frame)
	local parent = frame:getParent()
	local args = parent.args
	local ch_args = frame.args --для отладки
    local arb_page = ch_args[1] or mw.title.getCurrentTitle().fullText
    arb_page = string.gsub( arb_page, "%/Предытоги", "")
    arb_page = string.gsub( arb_page, "%/Форум", "/Голосование")
    local err, vote_table = {}, {}
	local candidates, electoratrue, exceptions, valid_votes, pre_result, pre_sort

	err, candidates = cand_listing (err, arb_page)
	err, electoratrue = elec_listing (err, arb_page)
	err, exceptions = caret(err, arb_page .. "/Исключения")
	local result = {}
	table.insert(result, table_start)

	if #candidates > 0 then
		for _, candidate in ipairs(candidates) do
			err, vote_table = vote_listing (err,vote_table,arb_page,candidate)
		end
		
		-- TODO function under construction
		err, valid_votes = compute (err,vote_table,electoratrue,exceptions) --,date
	--    mw.logObject(err)
		
		
		err, pre_result, pre_sort = reform (err, valid_votes)
	    -- TODO новая функция - недоделки
		for i, cand in ipairs(pre_sort) do
			table.insert(result, line_format(i, cand[1], pre_result[cand[1]], arb_page))
		end
		--mw.logObject(pre_result)
		--]]
	else
		table.insert(err,{"no data","elec","","Отсутствуют кандидаты"})
	end
	
	local err_notice = {}
	for i, err in ipairs(err) do
		err_notice[err[1]] = err_notice[err[1]] or {} -- err type
		err_notice[err[1]][err[2]] = err_notice[err[1]][err[2]] or {} -- err group
		err_notice[err[1]][err[2]][err[3]] = err_notice[err[1]][err[2]][err[3]] or {} -- err subgroup
		table.insert(err_notice[err[1]][err[2]][err[3]],err[4]) -- err messaage
	end

	local err_mass = {}
	for e_type, gr_errs in pairs(err_notice) do
		table.insert(err_mass,err_type[e_type])
		for e_group, subgr_errs in pairs(gr_errs) do 
			table.insert(err_mass,e_group)
			for e_subgr,e_msgs in pairs(subgr_errs) do
				table.insert(err_mass,e_subgr)
				for i, e_msg in ipairs(e_msgs) do
					table.insert(err_mass,e_msg)
				end
			end
		end
	end
--	mw.logObject(err_mass)
	if type(err_mass) == "table" and err_mass[1] then
		table.insert(err_mass,1,err_start)
		table.insert(err_mass,err_end)
		table.insert(result,table.concat(err_mass," "))
	end

	table.insert(result, table_end)
	
	return table.concat(result)
end

return p