############################################################################ # Copyright 2009-2019 Benjamin Kellermann # # # # This file is part of Dudle. # # # # Dudle is free software: you can redistribute it and/or modify it under # # the terms of the GNU Affero General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # # WARRANTY; without even the implied warranty of MERCHANTABILITY or # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # # License for more details. # # # # You should have received a copy of the GNU Affero General Public License # # along with dudle. If not, see . # ############################################################################ require_relative "hash" require "yaml" require "time" require_relative "pollhead" require_relative "timepollhead" $KCODE = "u" if RUBY_VERSION < '1.9.0' class String @@htmlidcache = {} @@htmlidncache = {} # FIXME: htmlid should not depend on the order it is requested def to_htmlID if @@htmlidcache[self] id = @@htmlidcache[self] else id = self.gsub(/[^A-Za-z0-9_\-]/,"_") if @@htmlidncache[id] @@htmlidncache[id] += 1 id += @@htmlidncache[id].to_s end @@htmlidncache[id] = -1 @@htmlidcache[self] = id end return id end end class WrongPollTypeError < StandardError end class Poll attr_reader :head, :name YESVAL = "a_yes__" MAYBEVAL = "b_maybe" NOVAL = "c_no___" @@table_html_hooks = [] def Poll.table_html_hooks @@table_html_hooks end def initialize name,type @name = name case type when "normal" @head = PollHead.new when "time" @head = TimePollHead.new else raise(WrongPollTypeError, "unknown poll type: #{type}") end @data = {} @comment = [] store "Poll #{name} created" end def sort_data fields if fields.include?("name") until fields.pop == "name" end @data.sort{|x,y| cmp = x[1].compare_by_values(y[1],fields) cmp == 0 ? x[0].downcase <=> y[0].downcase : cmp } else @data.sort{|x,y| x[1].compare_by_values(y[1],fields)} end end def userstring(participant,link) ret = "" if link ret += "" ret += " CGI.escapeHTML(participant)} ret += "\" href=\"?edituser=#{CGI.escape(participant)}\">" ret += EDIT ret += " | CGI.escapeHTML(participant)} ret += "\" href=\"?deleteuser&edituser=#{CGI.escape(participant)}\">" ret += "#{DELETE}" ret += "" ret += "" else ret += "" end ret += "#{CGI.escapeHTML(participant)}" ret += "" ret end def to_html(showparticipation = true) # border=1 for textbrowsers ;--) ret = "\n" sortcolumns = $cgi.include?("sort") ? $cgi.params["sort"] : ["timestamp"] ret += "#{@head.to_html(sortcolumns)}" ret += "" sort_data(sortcolumns).each{|participant,poll| if $cgi["edituser"] == participant ret += participate_to_html else ret += "\n" ret += userstring(participant,showparticipation) @head.columns.each{|column| case poll[column] when nil value = UNKNOWN klasse = "undecided" when /yes/ # allow anything containing yes (backward compatibility) value = YES klasse = YESVAL when /no/ value = NO klasse = NOVAL when /maybe/ value = MAYBE klasse = MAYBEVAL end ret += "\n" } ret += "" ret += "\n" end } @@table_html_hooks.each{|hook| ret += hook.call(ret)} ret += "" # PARTICIPATE ret += participate_to_html unless @data.keys.include?($cgi["edituser"]) || !showparticipation # SUMMARY ret += "\n" @head.columns.each{|column| yes = 0 undecided = 0 @data.each_value{|participant| if participant[column] =~ /yes/ yes += 1 elsif !participant.has_key?(column) or participant[column] =~ /maybe/ undecided += 1 end } if @data.empty? percent_f = 0 else percent_f = 100.0*yes/@data.size end percent = "#{percent_f.round}%" unless @data.empty? if undecided > 0 percent += "-#{(100.0*(undecided+yes)/@data.size).round} %" end ret += "\n" } ret += "" ret += "
#{value}#{poll['timestamp'].strftime('%c')}
" + _("Total") + "#{yes}
\n" ret end def invite_to_html edituser = $cgi["edituser"] unless $cgi.include?("deleteuser") invitestr = _("Invite") namestr = _("Name") ret = < #{namestr} HEAD @data.keys.sort.each{|participant| has_voted = false @head.columns.each{|column| has_voted = true unless @data[participant][column].nil? } if edituser == participant ret += "" ret += add_participant_input(edituser) ret += save_input(edituser,invitestr) else ret += "" ret += userstring(participant,!has_voted) end ret += "" } unless @data.keys.include?(edituser) ret += "" ret += add_participant_input(edituser) ret += save_input(edituser,invitestr) ret += "" end ret += "" ret end def add_participant_input(edituser) return < END end def save_input(edituser, savestring, changestr = _("Save changes")) ret = "" if @data.include?(edituser) ret += "" ret += "
" else ret += "" end ret += "\n" end def participate_to_html ret = "\n" if $cgi.include?("deleteuser") && @data.include?($cgi["edituser"]) ret += deleteuser_to_html else ret += edituser_to_html end ret += "\n" end def deleteuser_to_html ret = "\n" ret += "#{CGI.escapeHTML($cgi["edituser"])}" ret += "" ret += _("Do you really want to delete user %{user}?") % {:user => CGI.escapeHTML($cgi["edituser"])} ret += "" ret += "" ret += save_input($cgi["edituser"], "", _("Confirm")) ret += "" ret end def edituser_to_html edituser = $cgi["edituser"] checked = {} if @data.include?(edituser) @head.columns.each{|k| checked[k] = @data[edituser][k]} else edituser = $cgi.cookies["username"][0] unless @data.include?($cgi.cookies["username"][0]) @head.columns.each{|k| checked[k] = NOVAL} end ret = "\n" ret += add_participant_input(edituser) @head.columns.each{|column| ret += "" [[YES, YESVAL],[NO, NOVAL],[MAYBE, MAYBEVAL]].each{|valhuman, valbinary| ret += < TR } ret += "
" } ret += save_input(edituser, _("Save")) ret += "\n" ret end def comment_to_html(editable = true) ret = "
" ret += "

" + _("Comments") if !@comment.empty? || editable if $cgi.include?("comments_reverse") ret += " #{REVERSESORT}" else ret += " #{SORT}" end if @comment.size > 5 ret += " #{GODOWN}" end ret += "

" if !@comment.empty? || editable unless @comment.empty? i = 0 # for commentanchor c = @comment.dup c.reverse! if $cgi.include?("comments_reverse") c.each{|time,name,comment| ret += "
" ret += "

" i += 1 ret += _("%{user} said on %{time}") % {:user => name, :time => time.strftime("%d.%m., %H:%M")} if editable ret += "" ret += " " ret += "" end ret += "

#{comment}
" ret += "
" } end if @comment.size > 5 ret += "#{GOUP}" end if editable # ADD COMMENT saysstr = _("says") submitstr = _("Submit comment") ret += <
#{saysstr} 

ADDCOMMENT end ret += "
\n" ret end def history_selectform(revision, selected) showhiststr = _("Show history items:") ret = <
#{showhiststr} " ret += "" if revision updatestr = _("Update") ret += <
FORM ret end def history_to_html(middlerevision,only) log = VCS.history if only != "" case only when "comments" match = /^Comment .*$/ when "participants" match = /^Participant .*$/ when "columns" match = /^Column .*$/ when "ac" match = /^Access Control .*$/ end log = log.comment_matches(match) end log.around_rev(middlerevision,11).to_html(middlerevision,only) end def add_participant(olduser, name, agreed) name.strip! if name == "" maximum = @data.keys.collect{|e| e.scan(/^Anonymous #(\d*)/).flatten[0]}.compact.collect{|i| i.to_i}.max maximum ||= 0 name = "Anonymous ##{maximum + 1}" end action = '' if @data.delete(olduser) action = "edited" else action = "added" end @data[name] = {"timestamp" => Time.now } @head.columns.each{|column| @data[name][column] = agreed[column.to_s] } store "Participant #{name.strip} #{action}" end def delete(name) if @data.has_key?(name) @data.delete(name) store "Participant #{name.strip} deleted" end end def store comment File.open("data.yaml", 'w') do |out| out << "# This is a dudle poll file\n" out << self.to_yaml out.chmod(0660) end VCS.commit(comment) end ############################### # comment related functions ############################### def add_comment name, comment @comment << [Time.now, CGI.escapeHTML(name.strip), CGI.escapeHTML(comment.strip).gsub("\r\n","
")] store "Comment added by #{name}" end def delete_comment deltime @comment.each_with_index{|c,i| if c[0].strftime("%s") == deltime store "Comment from #{@comment.delete_at(i)[1]} deleted" end } end ############################### # column related functions ############################### def delete_column column if @head.delete_column(column) store "Column #{column} deleted" return true else return false end end def edit_column(oldcolumn, newtitle, cgi) parsedtitle = @head.edit_column(oldcolumn, newtitle, cgi) store "Column #{parsedtitle} #{oldcolumn == "" ? "added" : "edited"}" if parsedtitle end def edit_column_htmlform(activecolumn, revision) @head.edit_column_htmlform(activecolumn, revision) end end if __FILE__ == $0 require 'test/unit' require 'cgi' require 'pp' SITE = "glvhc_8nuv_8fchi09bb12a-23_uvc" class Poll attr_accessor :head, :data, :comment def store comment end end #┌───────────────────┬─────────────────────────────────┬────────────┐ #│ │ May 2009 │ │ #├───────────────────┼────────┬────────────────────────┼────────────┤ #│ │Tue, 05 │ Sat, 23 │ │ #├───────────────────┼────────┼────────┬────────┬──────┼────────────┤ #│ Name ▾▴ │ ▾▴ │10:00 ▾▴│11:00 ▾▴│foo ▾▴│Last edit ▾▴│ #├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ #│Alice ^✍ │✔ │✘ │✔ │✘ │24.11, 18:15│ #├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ #│Bob ^✍ │✔ │✔ │✘ │? │24.11, 18:15│ #├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ #│Dave ^✍ │✘ │? │✔ │✔ │24.11, 18:16│ #├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ #│Carol ^✍ │✔ │✔ │? │✘ │24.11, 18:16│ #├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ #│total │3 │2 │2 │1 │ │ #└───────────────────┴────────┴────────┴────────┴──────┴────────────┘ class PollTest < Test::Unit::TestCase Y,N,M = Poll::YESVAL, Poll::NOVAL, Poll::MAYBEVAL A,B,C,D = "Alice", "Bob", "Carol", "Dave" Q,W,E,R = "2009-05-05", "2009-05-23 10:00", "2009-05-23 11:00", "2009-05-23 foo" def setup def add_participant(type,user,votearray) h = { Q => votearray[0], W => votearray[1], E => votearray[2], R => votearray[3]} @polls[type].add_participant("",user,h) end @polls = {} ["time","normal"].each{|type| @polls[type] = Poll.new(SITE, type) @polls[type].edit_column("","2009-05-05", {"columndescription" => ""}) 2.times{|t| @polls[type].edit_column("","2009-05-23 #{t+10}:00", {"columntime" => "#{t+10}:00","columndescription" => ""}) } @polls[type].edit_column("","2009-05-23 foo", {"columntime" => "foo","columndescription" => ""}) add_participant(type,A,[Y,N,Y,N]) add_participant(type,B,[Y,Y,N,M]) add_participant(type,D,[N,M,Y,Y]) add_participant(type,C,[Y,Y,M,N]) } end def test_sort ["time","normal"].each{|type| comment = "Test Type: #{type}" assert_equal([A,B,C,D],@polls[type].sort_data(["name"]).collect{|a| a[0]},comment) assert_equal([A,B,D,C],@polls[type].sort_data(["timestamp"]).collect{|a| a[0]},comment) assert_equal([B,C,D,A],@polls[type].sort_data([W,"name"]).collect{|a| a[0]},comment) assert_equal([B,A,C,D],@polls[type].sort_data([Q,R,E]).collect{|a| a[0]},comment+ " " + [Q,R,E].join("; ")) } end end class StringTest < Test::Unit::TestCase def test_htmlid assert_equal("foo.bar.", "foo bar ".to_htmlID); assert_equal("foo.bar.", "foo bar ".to_htmlID); assert_equal("foo.bar.0", "foo.bar ".to_htmlID); assert_equal("foo.bar.00", "foo.bar 0".to_htmlID); assert_equal("foo.bar.", "foo bar ".to_htmlID); assert_equal("foo.bar.1", "foo bar.".to_htmlID); assert_equal("foo.bar.2", "foo?bar.".to_htmlID); assert_equal("foo.bar.3", "foo bar?".to_htmlID); assert_equal("foo.bar.2", "foo?bar.".to_htmlID); end end end